Go の for range でループ変数のキャプチャができない(goroutine が同じ値を見る)
Go 1.21 以前は for range のループ変数が全反復で共有されるため、goroutine やクロージャ内で参照すると最終値だけを見てしまう。
Go 1.22 以降は各反復で新しい変数になる仕様。
古い環境では明示的にローカル変数へ写す。
#go#for-range#goroutine#closure#go1-22
公開:
要約
Go 1.21 以前の for range はループ変数が全反復で共有されるため、goroutine やクロージャに渡すと最終値だけを見る。
Go 1.22 以降は各反復で新しい変数が作られる仕様に変わったので、go.mod の go 1.22 以降を指定するのが根本解決。
古い環境では v := v でシャドウイングするか、goroutine の引数として値を渡す。
よくある原因
- Go 1.21 以前の挙動を前提に書いている:
for _, v := range items { go func() { handle(v) }() } go.modのバージョン指定がgo 1.21以下で、go 1.22のループ変数セマンティクスが効いていない- テーブル駆動テストで
tt := ttのシャドウイングを忘れ、t.Parallel()を呼んだ結果テストケースが上書きされる - ポインタを取って append している:
result = append(result, &v)で全要素が同じアドレスを指す
解決策
1. Go 1.22 以降を使う
go.mod を更新する。
module example.com/myapp
go 1.22Go 1.22 以降は各反復で新しい変数が作られるため、以下のコードが意図通り動く。
items := []int{1, 2, 3}
for _, v := range items {
go func() { fmt.Println(v) }()
}変更点の詳細は Go 1.22 Release Notes の language セクションに記載されている。
2. 旧バージョンではローカル変数にシャドウイング
for _, v := range items {
v := v // 各反復ごとに新しい v を作る
go func() { fmt.Println(v) }()
}3. goroutine の引数として値渡し
for _, v := range items {
go func(x int) { fmt.Println(x) }(v)
}引数を介すと goroutine 起動時の値がコピーされ、ループ変数の参照が残らない。
4. go vet で検出する
go vet ./...go vet の loopclosure チェックがキャプチャ問題を警告する。
CI に組み込んでおくと事前に気付ける。
テーブル駆動テストの t.Parallel() を使うケースは特に検出価値が高い。
5. テーブル駆動テストの並列化
for _, tt := range tests {
tt := tt // Go 1.21 以前では必須
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
check(t, tt.in, tt.want)
})
}Go 1.22 以降は tt := tt のシャドウイングは不要。go.mod を上げたタイミングで一斉に削除して構わない。