Go WaitGroups
A WaitGroup waits for a collection of goroutines to finish. Without synchronization, the main function may exit before goroutines complete. WaitGroup is the standard, correct solution for this — replacing the unreliable time.Sleep workaround.
How WaitGroup Works
WaitGroup has an internal counter:
wg.Add(n) → increase counter by n
wg.Done() → decrease counter by 1
wg.Wait() → block until counter reaches 0
WaitGroup Counter:
main calls wg.Add(3) → counter = 3
goroutine 1 calls wg.Done() → counter = 2
goroutine 2 calls wg.Done() → counter = 1
goroutine 3 calls wg.Done() → counter = 0
wg.Wait() unblocks → main continues
Basic WaitGroup Example
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // decrement counter when function returns
fmt.Printf("Worker %d starting\n", id)
// simulate work
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // increment before starting goroutine
go worker(i, &wg)
}
wg.Wait() // block until all workers call Done()
fmt.Println("All workers finished")
}
Output (order of workers may vary):
Worker 1 starting
Worker 3 starting
Worker 2 starting
Worker 1 done
Worker 3 done
Worker 2 done
All workers finished
WaitGroup Flow Diagram
main()
│
├── wg.Add(1) → go worker(1) ──┐
├── wg.Add(1) → go worker(2) ──┤ (running concurrently)
├── wg.Add(1) → go worker(3) ──┤
│ │
├── wg.Wait() ◄────── blocks ───┤
│ │
│ worker 1 calls wg.Done() ───┤
│ worker 2 calls wg.Done() ───┤
│ worker 3 calls wg.Done() ───┘
│
└── continues → "All workers finished"
Important Rules
| Rule | Reason |
|---|---|
Call wg.Add(1) before starting the goroutine | Avoid race where goroutine finishes before Add is called |
Always use defer wg.Done() | Guarantees Done is called even if the goroutine panics |
Pass WaitGroup as a pointer (*sync.WaitGroup) | All goroutines must share the same WaitGroup instance |
| Do not copy a WaitGroup after first use | Copying breaks the internal state |
WaitGroup with Results
Combine WaitGroup with a slice or channel to collect results from goroutines.
package main
import (
"fmt"
"sync"
)
func square(n int, results []int, wg *sync.WaitGroup) {
defer wg.Done()
results[n] = n * n
}
func main() {
const count = 5
results := make([]int, count)
var wg sync.WaitGroup
for i := 0; i < count; i++ {
wg.Add(1)
go square(i, results, &wg)
}
wg.Wait()
fmt.Println(results) // [0 1 4 9 16]
}
Key Points
- WaitGroup blocks
mainuntil all goroutines callDone() - Call
wg.Add(1)before launching each goroutine, not inside it - Use
defer wg.Done()at the start of each goroutine for safety - Always pass WaitGroup as a pointer to goroutines
- WaitGroup replaces
time.Sleepas the proper synchronization tool
