WaitGroups
TL;DR
A sync.WaitGroup is the standard way to block a function until a specific number of background goroutines have completely finished their work. It acts as a thread-safe counter.
Mental Model
How It Works
A WaitGroup maintains an internal counter.
Add(int): Increases the counter (e.g., callAdd(1)before starting a goroutine).Done(): Decreases the counter by 1 (called at the end of the goroutine).Wait(): Blocks the calling thread completely until the counter hits0.
Example
package main
import (
"fmt"
"sync"
"time"
)
func processTask(id int, wg *sync.WaitGroup) {
// Defers execution of Done() until the function exits
defer wg.Done()
fmt.Printf("Worker %d starting...\n", id)
time.Sleep(time.Millisecond * 10) // Simulate work
fmt.Printf("Worker %d done!\n", id)
}
func main() {
var wg sync.WaitGroup
// Start 3 concurrent workers
for i := 1; i <= 3; i++ {
// IMPORTANT: Call Add() in the main thread, BEFORE launching the goroutine!
wg.Add(1)
go processTask(i, &wg)
}
fmt.Println("Waiting for workers to finish...")
// Block until wg counter hits 0
wg.Wait()
fmt.Println("All workers completed successfully.")
}
Common Interview Questions
What happens if you pass a WaitGroup to a function by value instead of by pointer?
This is a massive bug! If you do func process(wg sync.WaitGroup), Go copies the WaitGroup. The worker will call Done() on its copy, but the main thread’s WaitGroup counter will never decrease, resulting in a Deadlock. Always pass WaitGroups by pointer (*sync.WaitGroup), or use a closure.
Why must you call wg.Add() outside of the goroutine?
If you put wg.Add(1) inside the goroutine, the main thread might reach wg.Wait() before the goroutine even has a chance to start and call Add(1). Since the counter is initially 0, Wait() will unblock instantly and the program will exit prematurely.