無名関数
関数リテラルは名前のない関数です。式なので、値を置ける場所ならどこにでも置けます。変数、引数、戻り値、構造体のフィールドなどです。
Goにアロー関数やラムダの省略記法はありません。書く方法は func(params) results { body } の完全な形だけです。
変数を捕捉する
関数リテラルは、それを囲む関数の変数を使えます。コピーはしません。クロージャと周りのコードは1つの変数を共有します。
捕捉された変数は、それを宣言した関数が戻った後でも、どこかのクロージャが参照している限り生き続けます。必要ならコンパイラが変数をヒープに移すので、自分で管理することはありません。
ジェネレータとカウンタ
関数からクロージャを返すと、返された関数それぞれが自分だけの状態を持ちます。
counter() を呼ぶたびに新しい n が作られるので、c1 と c2 は別々に数えます。外から n を読んだりリセットしたりはできないので、これは小さなカプセル化の形になります。
ループの中のクロージャ(Go 1.22以降)
Go 1.22より前は、for ループのループ変数はループ全体で1つでした。その中で作ったクロージャはみなその1つの変数を捕捉し、最後の値を見ていました。これはGoで最もよくあるクロージャのバグでした。
Go 1.22以降は反復ごとに独自のコピーが作られるので、以下のコードは 0 1 2 と表示します。
Go 1.21以前では、同じループを for i := 0; i < 3; i++ で書くと(整数に対するrangeも1.22で新しく入ったものです)3 3 3 と表示されました。新しいルールは go.mod の go 行に基づいてモジュールごとに適用されるので、古いモジュールは go 行を1.22以降に上げるまで古い挙動のままです。古いコードではループ内に i := i や v := v がまだ見られます。手作業での修正方法だったもので、今は不要です。
反復ごとのルールが適用されるのは、for 文自身が宣言する変数だけです。ループの前で宣言し、本体で更新する変数は、今でも共有される1つの変数です。
var last string
for _, s := range items {
last = s
handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}
クロージャとゴルーチン
関数リテラルで開始したゴルーチンもクロージャです。同じ共有のルールに加えて、いつもの並行処理のルールが当てはまります。複数のゴルーチンが捕捉した変数に書き込むなら、ミューテックスかチャネルが必要です。
これは常に 5050 と表示します。各ゴルーチンは自分の i を読み(Go 1.22のループのセマンティクス)、共有の total にロックの下で書き込みます。ミューテックスを外すと結果は予測できなくなり、go run -race がデータ競合として報告します。WaitGroupと競合検出器についてはゴルーチンのページを参照してください。
実際のコードでクロージャが出てくる場所
- ソートと検索:
slices.SortFunc、slices.IndexFunc、sort.Sliceはどれも関数リテラルを受け取ります。 - 遅延実行の後始末:
defer func() { ... }()は関数の終了時にブロックを実行し、関数の変数を読めます。deferのページを参照してください。 - HTTPミドルウェア:ハンドラを受け取り、それを包む新しい
http.HandlerFuncのリテラルを返す関数です。 - 設定:functional options(
func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } })は引数を捕捉したクロージャです。
引数とカウンタの両方を捕捉するミドルウェアの例です。
よくある間違い
- その時点の値が保存されると思う。 クロージャが読むのは、作られたときの値ではなく、実行されたときの変数の現在の値です。その時点の値がほしいなら、引数として渡します:
go func(v int) { ... }(x)。 - 再帰するリテラル。 関数リテラルは、同じ
:=で代入されている変数を通して自分自身を参照できません。先に変数を宣言します:var walk func(n int); walk = func(n int) { ... walk(n-1) }。 - ゴルーチンからの同期されていない書き込み。 捕捉は同期ではありません。共有される書き込みは保護します。
よくある質問
Goの無名関数とは何ですか?
名前のない関数リテラルです:func(x int) int { return x * 2 }。変数に代入したり、引数として渡したり、返したり、閉じ波かっこの後に () を付けてすぐに呼び出したりできます。Goに別のラムダ構文はなく、関数リテラルがラムダです。
Goのクロージャとは何ですか?
外側で宣言された変数を参照する関数リテラルです。関数はそれらの変数を生かし続け、囲んでいるスコープと共有するので、クロージャの中での変更は外から見え、その逆も同じです。変数はコピーされず、参照で捕捉されます。
Goのクロージャは変数を値で捕捉しますか、参照で捕捉しますか?
参照です。クロージャと周りのコードは同じ変数を使います。その時点の値がほしいなら、クロージャを作る前に値を新しい変数にコピーするか、引数として渡します。
Goのクロージャのループ変数のバグは直りましたか?
Go 1.22から直りました。for ループの各反復で新しいループ変数が宣言されるようになったので、ループ内で作ったクロージャやゴルーチンはその反復の値を見ます。これは go.mod が go 1.22 以降を指定しているモジュールに適用されます。古いコードでは回避策としてループ内に i := i がよく書かれています。害はありませんが、もう必要ありません。