A common WaitGroup mistake

August 15, 2025

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.


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Get daily content like this in your inbox!

Subscribe