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