Go: wg.Add はゴルーチンを起動する前に呼ぶ(Go 1.25 以降は wg.Go)
wg.Add(1) をゴルーチンの中で呼ぶと、wg.Wait() が先に走ってカウンタ 0 を見て、作業が終わる前に戻ってしまう。Add は go 文の前に呼ぶか、Go 1.25 の wg.Go(f) を使う。
ダメな書き方
go
var wg sync.WaitGroup
for _, job := range jobs {
go func() {
wg.Add(1) // ← ここだと Wait に間に合わないことがある
defer wg.Done()
process(job)
}()
}
wg.Wait() // まだ Add されていなければ即座に戻るゴルーチンがいつ走り出すかはスケジューラ次第なので、手元では動いて本番でたまに処理が抜ける、という再現しにくいバグになる。
正しい書き方
go
// 従来: 起動する側で Add してから go
for _, job := range jobs {
wg.Add(1)
go func() {
defer wg.Done()
process(job)
}()
}
wg.Wait()
// Go 1.25 以降: Add と Done をまとめてやってくれる
for _, job := range jobs {
wg.Go(func() { process(job) })
}
wg.Wait()注意点
- Go 1.25 から
go vetの waitgroup チェックが、ゴルーチン内でのwg.Addを警告する。CI でgo vetを回していれば検出できる。 wg.Goに渡す関数は panic しないこと(panic はそのままプロセスを落とす)。エラーを集めたいならgolang.org/x/sync/errgroupのg.Goを使う。- Go 1.22 以降はループ変数がイテレーションごとに作られるので、上の例で
jobを引数で渡し直す必要はない。