sync.WaitGroup
In the previous lesson main closed the kitchen without waiting for the cooks, and their work was lost. A real head chef does not do that. On the wall hangs a board with the number of orders still being cooked. An order comes in—add one; a cook hands over a dish—subtract one. The kitchen closes only when the board shows zero.
A counter of goroutines
In Go, that board is the sync.WaitGroup type from the sync package. It is a counter of goroutines that have not finished yet:
package main
import "fmt"
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("hello") // hello
}()
wg.Wait()
fmt.Println("main") // main
}Let's walk through it step by step:
wg.Add(1)writes one order on the board: the counter becomes1.go func() { … }()starts a goroutine, and it printshello.- On its way out of the goroutine,
defer wg.Done()subtracts one. - Meanwhile
wg.Wait()inmainwaits until the counter reaches zero. - Only after that does
mainprintmain.
So the program prints both lines on every run, and hello always comes first.
The packages here are imported with two import lines: this is the same as one line with parentheses.
Add, Done and Wait
A WaitGroup has three methods. The zero value of a WaitGroup is ready to use: its counter is zero.
Add(n)
The Add(n) method raises the counter by n.
Interviewers like to ask this: where do you call Add? The answer: before the go statement, in the goroutine that later calls Wait. If Add moves into the new goroutine, Wait may run before it, see zero and return while the work is not done. The chef closes the kitchen because the order was not on the board yet.
Done()
The Done() method lowers the counter by one. It is called with defer on the first line of the goroutine, so that it runs however the function exits.
If Done is called more times than Add added, the program panics with sync: negative WaitGroup counter. There is no such thing as a negative number of orders.
Wait()
The Wait() method waits until the counter reaches zero.
This is where the board picture stops working: a chef's board can say who is cooking what, while a WaitGroup holds a single number. It does not know which goroutines are still running and cannot tell you whom Wait is waiting for.
Results in a shared slice
Goroutines often write their results into a shared slice, each into its own element. Then the result does not depend on the order in which they ran:
var wg sync.WaitGroup
squares := make([]int, 3)
for i := range 3 {
wg.Add(1)
go func() {
defer wg.Done()
squares[i] = i * i
}()
}
wg.Wait()
fmt.Println(squares) // [0 1 4]Here is why the result is always the same:
- each goroutine puts its dish on its own shelf:
squares[0],squares[1]orsquares[2]; - no two goroutines ever write to the same element, so their order does not matter;
- after
Waitall three shelves are filled, and the program prints[0 1 4].
The Go method
Since Go 1.25, WaitGroup has a Go method. The call wg.Go(f) takes three steps:
- It raises the counter by one.
- It starts
fin a new goroutine. - It calls
Donewhenfreturns.
The loop above gets shorter:
var wg sync.WaitGroup
squares := make([]int, 3)
for i := range 3 {
wg.Go(func() {
squares[i] = i * i
})
}
wg.Wait()
fmt.Println(squares) // [0 1 4]You cannot get the order of Add and go wrong here: the method does both itself.
Common mistake
A WaitGroup was passed to a function by value:
func work(wg sync.WaitGroup) { // ✗
defer wg.Done()
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go work(wg)
wg.Wait()
}fatal error: all goroutines are asleep - deadlock!Here is what happens:
- The function got a copy of the board.
worklowers the counter of its own copy, and the counter inmainstays at one.Waitwaits forever, and when every goroutine is blocked, the runtime stops the program.
A WaitGroup is never copied: the function takes wg *sync.WaitGroup, and the call passes &wg.
Key points
- A
WaitGroupcounts unfinished goroutines:Add(n)adds,Done()subtracts one,Wait()waits for zero. Addis called before thegostatement;wg.Go(f)doesAdd,goandDoneitself.- Calling
Donemore often thanAddpanics withsync: negative WaitGroup counter. - A
WaitGroupis not copied: a function gets the pointer&wg.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.