Can you spot the mistake in this code?
for i := range 100 {
go func() {
wg.Add(1)
defer wg.Done()
fmt.Println(i)
}()
}
wg.Wait()
At first glance, this code probably looks fine. We’re calling wg.Add(1)
at the beginning of each task. We’re deferring the call to wg.Done()
, so it’s called when the function exits. Each Add
call has a matching Done
call. We’re properly waiting for all tasks to complete with wg.Wait()
. All boxes checked, right?
Not so.
Although this code follows a commonly observed pattern, and will work in some cases, it is actually incorrect. The problem is that wg.Add(1)
is called from within the newly spawned goroutine itself. This means that it’s possible for the goroutine to not have started executing before wg.Wait()
is called. If that happens, then wg.Wait()
will see a wait group counter of zero, and will return immediately, even though there are still goroutines running.
Enter Go 1.25’s new go vet
check (mentioned briefly yesterday). The full description of the new analyzer is in the GoDoc:
This analyzer detects mistaken calls to the (*sync.WaitGroup).Add method from inside a new goroutine, causing Add to race with Wait:
// WRONG var wg sync.WaitGroup go func() { wg.Add(1) // "WaitGroup.Add called from inside new goroutine" defer wg.Done() ... }() wg.Wait() // (may return prematurely before new goroutine starts)
The correct code calls Add before starting the goroutine:
// RIGHT var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() ... }() wg.Wait()
If you’re using Go 1.25, you can run go vet
on your code to find this and other common mistakes. If you’re using golangci-lint, you can enable the govet
linter to include this check. It’s a good idea to include this in your CI checks, and in your IDE to run when a file is saved, to catch this kind of mistake early.