Goでは、プログラムの実行時にパッケージ、変数、関数の初期化に関して厳格なルールがあります。主なメカニズムは、init関数の実行とグローバル変数の初期化です。これらのプロセスを正しく理解することは、エラーや予期しない効果を防ぐために重要です。
問題の歴史:
Goでは、昔から起動のフェーズを厳格に区別しています:宣言、初期化、およびその後のコードの実行。C/C++などの言語では、グローバル変数のコンストラクタがよく使用されますが、Goでは初期化の順序が決定論的ですが、いくつかのニュアンスがあります。
問題:
グローバル変数の初期化やinitの呼び出しが、パッケージ間で相互依存または循環する状況に陥るのは簡単です。それを追跡するのは難しく、プログラムは開発者が期待するようには動作しない可能性があります。特に隠れた依存関係や開始時の状態の密閉がある場合はそうです。
解決策:
Goのパッケージは、その依存関係に基づいて順番に初期化されます:まず依存関係、次に自パッケージです。最初にpackage-levelの変数が初期化され(ソースファイルに現れる順)、次にinit()関数があればそれが呼び出されます。1つのファイルで複数のinit()を宣言することもできます。1つのパッケージ内のファイル間の初期化順序は未定義です(これがエラーを引き起こす可能性があります)。
コード例:
// a.go package main import "fmt" func init() { fmt.Println("a.goからのinit") } // b.go package main import "fmt" func init() { fmt.Println("b.goからのinit") }
これらのinit関数の実行結果は、同じディレクトリ内のファイル間で予測不可能ですが、常にmain()関数の前に実行されます。
主な特徴:
同じパッケージ内の異なるファイルのinit関数の実行順序を信頼できますか?
いいえ!Goは、同じパッケージ内の異なるファイルのinit関数の順序を保証していません。特定の順序に対する期待は、捕らえにくいエラーやビジネスロジックの崩壊に繋がる可能性があります。
init関数の実行時にグローバル変数が初期化されていない可能性はありますか?
いいえ — パッケージのすべてのグローバル変数は、すべてのinit関数の前に厳密に宣言された順序で実行されます。例外はパッケージ間の相互初期化のみです(下記参照)。
パッケージ間のinitの循環依存を避けるにはどうすればよいですか?
Goはパッケージ間の循環インポートを許可しません(これはコンパイル時エラーです)が、間接的な初期化の罠にはまる可能性があります:AがBに依存し、BがCに依存し、C(グローバル変数またはinitを通じて)がAからコードを呼び出す場合です。このような場合、不明瞭なinit/グローバルコンストラクタの呼び出し順序が発生する可能性があります。
サービスの初期化ロジックが異なるファイルの複数のinit関数で実行されます。1つのinitが他の結果に依存しているため、異なるサーバー間でのビルドや起動時にランダムな動作を引き起こします。
利点:
欠点:
すべての状態と初期化がmain()内の明示的な呼び出しで実行されます。init関数は起動のトレースや小さな確認にのみ使用されます。
利点:
欠点: