Goではグローバル変数はファイルの宣言順序とそれぞれのinit関数の順序に従って初期化されます。これによりグローバルな状態へのアクセスの正確性が依存します:あるパッケージの変数が別のパッケージの変数の値に依存している場合、そのパッケージがまだ初期化されていないとエラーまたは予期しない動作が発生します。
異なるパッケージのinit関数間での「スタート順序」を直接指定することはできません。コンパイラはインポートグラフに基づいてこの順序を決定します。
main.main()関数を呼び出す前に初期化されます。安全な遅延初期化(lazy init)の例:
var cfg *Config var once sync.Once func GetConfig() *Config { once.Do(func() { cfg = LoadConfigFromDisk() }) return cfg }
質問:Goプログラムを起動する際に、異なるパッケージのinit関数は同時に(並行に)実行されることがありますか?
正しい答え: いいえ、init関数とグローバル変数は、パッケージ間の依存関係を解析した際にコンパイラによって決定された順序で厳密に連続して実行されます。init関数の並行実行は発生しません。
物語
大規模プロジェクトで、複数のモジュールがinit()を通じてディスクからグローバル設定を読み込んでいました。一つのモジュールは別のモジュールに依存していましたが、go.modのグラフの再編成により初期化順序が変更され、突然設定値が空になってしまいました!このエラーはビルド順に依存して偶発的に発生し、依存関係を分析して初期化を明示的な関数に移すまで発見されませんでした。
物語
RESTサービスでは、整合表のキャッシングにグローバルマップがmutexなしで使用されていました。初期化はinit内で起動された複数のgoroutineから開始されていました。その結果、データレース、定期的なパニック、およびマップ内の不正なデータが発生しました。sync.Onceに置き換え、初期化を明示的に呼び出すことで問題は解決しました。
物語
グローバルロガーは一つの補助パッケージのinit関数で作成されましたが、別のパッケージはその初期化前にロガーにアクセスしていました。結果として、ロガーが存在しない間に開始に関する一部のログが失われました。解決策は依存関係の注入と、すべてのサービスを起動する前にロガーを明示的に初期化することです。