クロージャがループ外の変数を共有キャプチャして最終値しか残らない
forループの外側で宣言した変数をクロージャの中で使い、そのクロージャをスライスに貯めていく場合、各クロージャは同じ変数を参照し続けます。実行時にはループ終了後の最終値だけが全クロージャに共通して見えます。
なぜエラーが出ないのか
出力: [3 3 3]
(エラーなし)- Goは何も報告しません。コンパイルも実行も正常に終わるコードだからです
出力: [3 3 3]- 実際の出力 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 package main 2 3 import "fmt" 4 5 func main() { 6 var funcs []func() int 7 sum := 0 8 for i := 0; i < 3; i++ { 9 sum += i 10 s := sum 11 _ = s 12 funcs = append(funcs, func() int { return sum }) ^ 13 } 14 var results []int 15 for _, f := range funcs { 16 results = append(results, f()) 17 } 18 fmt.Println(results) 19 }
sumはループの外で宣言された1つの変数で、全クロージャに共有されます。ループの各回でs := sumのように新しい変数へコピーしてから使う必要があります。
直し方: sum を s にします。
パターン2
1 package main 2 3 import "fmt" 4 5 func main() { 6 var funcs []func() int 7 sum := 0 8 for i := 0; i < 3; i++ { 9 sum += i * 2 10 s := sum 11 _ = s 12 funcs = append(funcs, func() int { return sum }) ^ 13 } 14 var results []int 15 for _, f := range funcs { 16 results = append(results, f()) 17 } 18 fmt.Println(results) 19 }
加算の中身が変わっても、sumが1つの変数を全クロージャで共有している構造自体は変わりません。
直し方: sum を s にします。
パターン3
1 package main 2 3 import "fmt" 4 5 func main() { 6 var funcs []func() int 7 sum := 0 8 for i := 0; i < 3; i++ { 9 sum += 10 10 s := sum 11 _ = s 12 funcs = append(funcs, func() int { return sum }) ^ 13 } 14 var results []int 15 for _, f := range funcs { 16 results = append(results, f()) 17 } 18 fmt.Println(results) 19 }
iを使わない定数加算でも、共有変数を直接クロージャに使えば同じ問題が起きます。
直し方: sum を s にします。
パターン4
1 package main 2 3 import "fmt" 4 5 func main() { 6 var funcs []func() int 7 sum := 0 8 for i := 0; i < 3; i++ { 9 sum += i + 1 10 s := sum 11 _ = s 12 funcs = append(funcs, func() int { return sum }) ^ 13 } 14 var results []int 15 for _, f := range funcs { 16 results = append(results, f()) 17 } 18 fmt.Println(results) 19 }
クロージャが呼ばれるのはループが終わった後なので、共有変数の値は常に最終状態のまま観測されます。
直し方: sum を s にします。
パターン5
1 package main 2 3 import "fmt" 4 5 func main() { 6 var funcs []func() int 7 sum := 0 8 for i := 0; i < 4; i++ { 9 sum += i 10 s := sum 11 _ = s 12 funcs = append(funcs, func() int { return sum }) ^ 13 } 14 var results []int 15 for _, f := range funcs { 16 results = append(results, f()) 17 } 18 fmt.Println(results) 19 }
ループの回数が増えても、共有変数を直接参照している限り全クロージャが同じ最終値を返します。
直し方: sum を s にします。
よくある誤解
「クロージャを作った時点の値がそれぞれ個別に固定されるはず」という思い込みは誤りです。Go 1.22以降でforの各回のループ変数自体は新しいインスタンスになりましたが、ループの外で宣言してループ内で使い回している変数はこの対象外で、依然として全クロージャに共有されます。
実務での勘所
Go 1.22で変更されたのは、for i := 0; ...のようにfor文自身が宣言するループ変数だけです。この場合、周回ごとに新しいiの実体が作られるようになり、クロージャはそれぞれ自分の周回のiを正しく覚えられるようになりました。しかしこのトピックが扱っているのは、ループの外で宣言してループの中で使い回している別の変数です。この変数はfor文自身が管理するループ変数ではないため、Go 1.22以降の変更の対象外であり、従来どおり全てのクロージャから同じ1つの実体として共有され続けます。