Goでは匿名関数(関数リテラル)がクロージャを生成することができ、これは周囲のスコープから変数にアクセスできることを意味します。クロージャは、処理が正しく動作するために必要な場合にはヒープにメモリを割り当てます(エスケープ解析によって検出されます)。
例:
func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } a := adder() printf("%d\n", a(5)) // 5 printf("%d\n", a(10)) // 15
特性:
このコードは何を出力しますか?
func main() { fs := []func(){} for i := 0; i < 3; i++ { fs = append(fs, func() { fmt.Println(i) }) } for _, f := range fs { f() } }
多くの人は0, 1, 2を出力すると答えるかもしれませんが、結果は以下の通りです:
3
3
3
すべてのクロージャは同じ変数iを参照しており、ループの終了時にその値は3です。
正しい方法: ループの本体で変数のコピーをキャプチャすることです:
for i := 0; i < 3; i++ { v := i // 新しい変数 fs = append(fs, func() { fmt.Println(v) }) }
物語
動的ルーティングプロジェクトで、複数のハンドラをクロージャを使用して作成するためにループを使用しましたが、各ハンドラは独自のパスをキャプチャする必要がありました。その結果、すべてのハンドラが最後のパスを出力しました — 各クロージャに個別の変数を作成しませんでした。この誤りはHTTP APIとの統合時に発覚しました。
物語
ゴルーチンを介した並列アクセスのテスト中、クロージャはインデックスの参照をキャプチャし、コピーをキャプチャしませんでした。これにより「ランダム」な効果が生じ、データが自分のスロットには保存されず、最後のスロットに保存されました。
物語
統計収集関数内で、クロージャは外部スコープの共有変数を変更しましたが、作成者は各タスクの独立したカウンタを期待していました。この問題は、常に共通の(プライベートではない)再構成できない合計を見て発見されました。