What is sync.WaitGroup, and how do you use it correctly?
Quick Answer
sync.WaitGroup lets one goroutine wait for a group of other goroutines to finish. Call wg.Add(n) to register n goroutines up front, wg.Done() (usually deferred) inside each goroutine when it finishes, and wg.Wait() in the waiting goroutine to block until the count reaches zero. Common misuses: calling Add after starting the goroutine (a race, since Wait might run before Add registers it), and passing a WaitGroup by value to a function instead of by pointer, which copies its internal counter and breaks the coordination entirely — a WaitGroup must always be passed by pointer, or embedded and accessed through a pointer receiver, once it's shared across function boundaries.
Detailed Answer
WaitGroup is simple in concept, but two specific mistakes account for almost every bug involving it.
Correct basic usage
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println("worker", id, "done")
}(i)
}
wg.Wait() // blocks until all 5 call Done()
Note Add(1) happens in the loop, before go func(...) starts — registering the count before the goroutine launches.
Mistake 1: calling Add too late
var wg sync.WaitGroup
go func() {
wg.Add(1) // BUG: race — Wait() below might already be past its check
defer wg.Done()
doWork()
}()
wg.Wait()
If Wait() runs before the goroutine gets to Add(1), the count is still zero, and Wait() returns immediately, before the work is actually done.
Mistake 2: passing WaitGroup by value
func startWorkers(wg sync.WaitGroup) { // BUG: value receiver copies the WaitGroup
wg.Add(1) // increments the copy's counter, not the caller's
go func() {
defer wg.Done()
doWork()
}()
}
func startWorkers(wg *sync.WaitGroup) { // correct: pointer, shares the same counter
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
}
go vet catches this specific mistake (copying a sync.WaitGroup) automatically, which is one reason go vet should always run as part of CI.
The idiomatic pattern
Add before starting the goroutine, defer Done() as the first line inside it, and WaitGroup always shared by pointer. Following this exact shape avoids both common bugs.