Menu

Go言語のクロージャと無名関数を例で解説

Goの無名関数は、周りのスコープの変数を捕捉できます。それがクロージャになり、カウンタ、ジェネレータ、ミドルウェア、独自の状態を持つコールバックに使われます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

無名関数

関数リテラルは名前のない関数です。式なので、値を置ける場所ならどこにでも置けます。変数、引数、戻り値、構造体のフィールドなどです。

Goにアロー関数やラムダの省略記法はありません。書く方法は func(params) results { body } の完全な形だけです。

変数を捕捉する

関数リテラルは、それを囲む関数の変数を使えます。コピーはしません。クロージャと周りのコードは1つの変数を共有します。

捕捉された変数は、それを宣言した関数が戻った後でも、どこかのクロージャが参照している限り生き続けます。必要ならコンパイラが変数をヒープに移すので、自分で管理することはありません。

ジェネレータとカウンタ

関数からクロージャを返すと、返された関数それぞれが自分だけの状態を持ちます。

counter() を呼ぶたびに新しい n が作られるので、c1c2 は別々に数えます。外から 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.modgo 行に基づいてモジュールごとに適用されるので、古いモジュールは go 行を1.22以降に上げるまで古い挙動のままです。古いコードではループ内に i := iv := 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.SortFuncslices.IndexFuncsort.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.modgo 1.22 以降を指定しているモジュールに適用されます。古いコードでは回避策としてループ内に i := i がよく書かれています。害はありませんが、もう必要ありません。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める