Goのforループ構文には、初期化ブロック(init)、条件チェック、ポストフィックス式を含めることができます。このメカニズムは、コード記述の利便性とC系言語の慣習から生まれたものです。しかし、Goではループ変数(i)のスコープが特有で、ネストされた関数、クロージャ、ゴルーチン内の動作に大きな影響を与えます。
問題 — ゴルーチンやクロージャをループの各反復で開始すると、予期しない動作がよく起こります:変数iはコピーされず、参照で「キャプチャ」されるため、クロージャはループの共有変数を参照し、ループ終了後に最後の値を取ります。これにより、すべてのゴルーチン/クロージャで同じ結果が得られますが、論理的には異なるはずです。
解決策 — 反復ごとの変数の値を渡す必要がある場合は、変数を明示的にコピーする(追加の変数を使用する)か、クロージャに引数として渡します。
コードの例:
for i := 0; i < 3; i++ { go func(j int) { fmt.Println(j) }(i) // 正しい! コピーされた値 } for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // エラー:すべてのゴルーチンが3を出力します }
重要な特徴:
breakまたはcontinueを使用する際にforの変数のスコープは変わりますか?
いいえ。forで宣言された変数のスコープは、常にそのループブロック内に制限されます。Breakまたはcontinueは次の反復を中断するだけで、変数を外部に「持ち出す」ことはありません。
forのinit部分で宣言された変数をループ外のメソッドでキャプチャできますか?
いいえ。変数はfor自体とその内部ブロック内でのみ可視ですが、ループ終了後は外部では見えません。
defer式内で変数がキャプチャされた場合はどうなりますか?
同じ状況です:defer関数は作成時ではなく、deferを実行する時点での変数の現在の値(通常はループの最後の値)を「見る」ことになります。
for i := 0; i < 3; i++ { defer fmt.Println(i) // すべてのdeferが3を出力します }
#一般的なミスとアンチパターン
GoのWebサーバーでは、開発者が異なるポートを処理するために複数のゴルーチンを起動し、ポートのインデックスをループ変数として使用し、そのままラムダ式でキャプチャしていました。すべてのゴルーチンが配列の最後のポートにアクセスしていました。
利点:
欠点:
チームは、常にループ変数の値を新しい変数にコピーし、その後でクロージャ/ゴルーチンでキャプチャするルールを導入しました。
利点:
欠点: