ProgrammingシニアGo開発者

Goではクロージャ(関数リテラル/クロージャ)がどのように実現されているか、クロージャの使用に関する制限と特性:それらはどこに保存され、変数はどのようにキャプチャされ、異なるシナリオでキャプチャされた変数の挙動はどのように異なるかについて説明してください。

Hintsage AIアシスタントで面接を突破

回答

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との統合時に発覚しました。


物語

ゴルーチンを介した並列アクセスのテスト中、クロージャはインデックスの参照をキャプチャし、コピーをキャプチャしませんでした。これにより「ランダム」な効果が生じ、データが自分のスロットには保存されず、最後のスロットに保存されました。


物語

統計収集関数内で、クロージャは外部スコープの共有変数を変更しましたが、作成者は各タスクの独立したカウンタを期待していました。この問題は、常に共通の(プライベートではない)再構成できない合計を見て発見されました。