できない.dev

Go の for range でループ変数のキャプチャができない(goroutine が同じ値を見る)

Go 1.21 以前は for range のループ変数が全反復で共有されるため、goroutine やクロージャ内で参照すると最終値だけを見てしまう。
Go 1.22 以降は各反復で新しい変数になる仕様。
古い環境では明示的にローカル変数へ写す。

公開: 更新:

実行例あり(2026-09-22 に実環境で検証)

要約

Go 1.21 以前の for range はループ変数が全反復で共有されるため、goroutine やクロージャに渡すと最終値だけを見る。
Go 1.22 以降は各反復で新しい変数が作られる仕様に変わったので、go.mod の go 1.22 以降を指定するのが根本解決。
古い環境では v := v でシャドウイングするか、goroutine の引数として値を渡す。

実行例

golang:1.23 コンテナ(Go 1.23.12)で go.mod を go 1.21 にしたまま動かすと、クロージャ 3 つがすべて closure: 3 を出し、go vet は goroutine の中で v を参照している 26 行目だけを「loop variable v captured by func literal」と報告して終了コード 1 になる。
ループ内で v := v と写すか、go mod edit -go=1.22 で go ディレクティブを上げると、どちらも closure: 1、2、3 と反復ごとの値が出た。

$ cat go.mod
module example.com/loopdemo
 
go 1.21
$ go run .
closure: 3
closure: 3
closure: 3
$ go vet ./...
# example.com/loopdemo
# [example.com/loopdemo]
./main.go:26:8: loop variable v captured by func literal
終了コード: 1
$ go run .
closure: 1
closure: 2
closure: 3
$ go mod edit -go=1.22
$ cat go.mod
module example.com/loopdemo
 
go 1.22
$ go run .
closure: 1
closure: 2
closure: 3
終了コード: 0

— 2026-09-22 時点の出力

検証環境

検証日
実行環境
golang:1.23Debian GNU/Linux 12 (bookworm)
バージョン
  • Python 3.11.2
  • Git 2.39.5
  • Go 1.23.12

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

よくある原因

  1. Go 1.21 以前の挙動を前提に書いている: for _, v := range items { go func() { handle(v) }() }
  2. go.mod のバージョン指定が go 1.21 以下で、go 1.22 のループ変数セマンティクスが効いていない
  3. テーブル駆動テストで tt := tt のシャドウイングを忘れ、t.Parallel() を呼んだ結果テストケースが上書きされる
  4. ポインタを取って append している: result = append(result, &v) で全要素が同じアドレスを指す

解決策

1. Go 1.22 以降を使う

go.mod を更新する。

module example.com/myapp
 
go 1.22

Go 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 チェックがキャプチャ問題を警告する。
ただし検出するのは、ループ本体の最後の文にある go / defer の関数リテラルや、t.Parallel() を呼ぶ t.Run などの限られた形だけで、クロージャをスライスに溜めて後から呼ぶ形や &v を append する形(原因 4)は報告されない。
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 を上げたタイミングで一斉に削除して構わない。

この記事は役立ちましたか?