Another solution to the WaitGroup mistake

August 18, 2025

Join me again, this coming Friday, August 22, at 3:00pm EDT (19:00 UTC), more live coding. Subscribe on YouTube and hit the Notify me button so you don’t miss it!


Last week we looked at a common mistake with sync.WaitGroup, and how Go 1.25’s new go vet check can help catch it. To recap, the mistake is calling wg.Add(1) from within a goroutine, which can cause wg.Wait() to return prematurely:

for i := range 100 {
	go func() {
		wg.Add(1)
		defer wg.Done()
		fmt.Println(i)
	}()
}

wg.Wait()

But Go 1.25 also introduced another solution to the same problem. One I’m even more excited about, because it make the code more readable and easier to understand at the same time.

Introducing… sync.WaitGroup.Go!

func (*WaitGroup) Go ¶

func (wg *WaitGroup) Go(f func())

Go calls f in a new goroutine and adds that task to the WaitGroup. When f returns, the task is removed from the WaitGroup.

The function f must not panic.

If the WaitGroup is empty, Go must happen before a WaitGroup.Wait. Typically, this simply means Go is called to start tasks before Wait is called. If the WaitGroup is not empty, Go may happen at any time. This means a goroutine started by Go may itself call Go. If a WaitGroup is reused to wait for several independent sets of tasks, new Go calls must happen after all previous Wait calls have returned.

In the terminology of the Go memory model, the return from f “synchronizes before” the return of any Wait call that it unblocks.

This means we can rewrite our example like this:

for i := range 100 {
  wg.Go(func() {
    fmt.Println(i)
  })
}

wg.Wait()

Isn’t that so much cleaner? I, for one, think so.

This new method automatically handles the wg.Add(1) and wg.Done() calls for us, making the code less error-prone and easier to read. It also ensures that the goroutine is properly registered with the WaitGroup before it starts executing.

When would one ever use Add() or Done() directly after this change? To be honest, probably rarely. But there are still some cases when you might want to. When adding multiple tasks at once, for example:

wg.Add(100)
for i := range 100 {
	go func() {
		defer wg.Done()
		fmt.Println(i)
	}()
}

wg.Wait()

Or maybe if you know the number of tasks in advance and want to set it up before starting the goroutines:

wg.Add(100)
for i := range <- inputChannel {
	go func() {
		defer wg.Done()
		fmt.Println(i)
	}()
}

Share this

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

Unsure? Browse the archive .

Get daily content like this in your inbox!

Subscribe