What is the classic sync.WaitGroup bug, and what does wg.Go (Go 1.25) change?
Question 225MediumGo 1.22 to 1.25
The classic bug is calling Add inside the goroutine:
// BUG: Wait may run before any Add, and return immediately
for _, j := range jobs {
go func() {
wg.Add(1)
defer wg.Done()
process(j)
}()
}
wg.Wait()
Add must happen before the go statement, so that it happens-before Wait. Other rules:
- A negative counter panics.
- You may reuse a WaitGroup only after the previous
Waithas returned. - Pass it by pointer, never by value.
Go 1.25 added WaitGroup.Go, which does Add(1), starts the goroutine, and defers Done for you:
var wg sync.WaitGroup
for _, j := range jobs {
wg.Go(func() { process(j) })
}
wg.Wait()
This removes the misplaced-Add and forgotten-Done bugs. Go 1.25's go vet also has a waitgroup analyzer that flags wg.Add(1) inside the goroutine.
What WaitGroup does not do: it does not collect errors or cancel the other goroutines. When you need either, use errgroup.
More on Concurrency Patterns & sync
- Q223Is sync.Mutex reentrant? What happens with a recursive RLock on an RWMutex?
- Q224What does this print, and why? (copying a lock)
- Q226Explain sync.Once semantics, including panics, and the OnceFunc / OnceValue / OnceValues helpers.
- Q227When would you use sync.Cond, and why must Wait always be called in a loop?
- Q228How does sync.Pool work, how does it interact with the GC, and what are the common misuse pitfalls?
- Q229When should you use sync.Map instead of a map guarded by a Mutex?