Oops, did it again: sync.WaitGroup race condition

Here’s a short description of a common mistake when using sync.WaitGroup. A proposal from 2016 to detect this issue with go vet checks has recently been accepted for Go 1.25. Links below.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	for i := 1; i <= 5; i++ {
		go func() {
			wg.Add(1)
			fmt.Printf("Finished: %d\n", i)
			wg.Done()
		}()
	}

	wg.Wait()
	fmt.Println("All routines complete")
}

Results:

mac:~ user$ go run main.go 
All routines complete
Finished: 1
mac:~ user$ go run main.go 
All routines complete
Finished: 3

What was expected? What happened?

Expected

At first glance what seems should happen is that each of the 5 go routines should print and then the application exit:

mac:~ user$ go run main.go 
Finished: 5
Finished: 3
Finished: 2
Finished: 4
Finished: 1
All routines complete

What Happened?

We can see from the output of actually running the code the expected result does not happen. This is because the wg.Add(1) occurs inside of the go routine.

	go func() {
		wg.Add(1)
		fmt.Printf("Finished: %d\n", i)
		wg.Done()
	}()

If the machine is fast enough the wg.Wait() can be reached by the main routine before each of the go routines have had time to actually add themselves to the wait group with wg.Add(1).

The simple fix is to move the wg.Add(1) before the go routine is started:

	for i:=1; i <= 5; i++ {
	wg.Add(1)
	go func() {
			defer wg.Done()
			fmt.Print("Finished: %d",i)		
	}

References:

Issue(2016): https://github.com/golang/go/issues/18022 Proposal: https://github.com/golang/go/issues/63796 Changes: https://go-review.googlesource.com/c/go/+/661519 Cup-O-Go Podcast: https://cupogo.dev/episodes/one-and-two-and-three-and-four-and-proposals