WaitGroup
sync.WaitGroup waits for a collection of goroutines to finish.
Introduction
sync.WaitGroup waits for a collection of goroutines to finish. Call Add before launching, Done in defer inside goroutine, and Wait in the coordinator. It is simpler than channels when you only need completion signaling, not data transfer.
Common pattern: fan-out work to N goroutines, WaitGroup waits for all, then aggregate results. errgroup.Group extends this with first-error cancellation — covered in advanced production code.
WaitGroup misuse (Add after Wait, negative counter) panics — this lesson covers correct lifecycle and alternatives.
The story
Before deploying a Terraform plan across 50 AWS regions, a platform tool launches one goroutine per region to validate state files, then waits with sync.WaitGroup before printing a summary. The main goroutine calls wg.Wait() only after every regional check calls wg.Done() — preventing premature exit that would kill in-flight validations.
WaitGroups are the simplest coordination primitive for fan-out/fan-in batch jobs in CI pipelines and data migration tools.
Understanding the topic
Key concepts
- var wg sync.WaitGroup; wg.Add(n) before starting goroutines.
- defer wg.Done() at start of each goroutine worker.
- wg.Wait() blocks until counter reaches zero.
- Add must happen before goroutine starts — or race detector fires.
- WaitGroup is not reusable until Wait returns.
- errgroup: golang.org/x/sync/errgroup for error propagation.
Step-by-step explanation
- Counter increments with Add, decrements with Done.
- Wait blocks while counter > 0.
- Each goroutine calls Done exactly once (defer ensures this).
- Pass &wg to goroutine or capture in closure.
- Wait after all workers launched — main coordination point.
- Combine with channels for results after Wait completes.
Practical code example
Parallel URL health checks with WaitGroup:
package mainimport ("fmt""net/http""sync""time")func checkURL(url string, wg *sync.WaitGroup, results chan<- string) {defer wg.Done()client := &http.Client{Timeout: 2 * time.Second}resp, err := client.Get(url)if err != nil {results <- fmt.Sprintf("%s: FAIL (%v)", url, err)return}resp.Body.Close()results <- fmt.Sprintf("%s: OK (%d)", url, resp.StatusCode)}func main() {urls := []string{"https://go.dev", "https://google.com"}var wg sync.WaitGroupresults := make(chan string, len(urls))for _, u := range urls {wg.Add(1)go checkURL(u, &wg, results)}go func() {wg.Wait()close(results)}()for r := range results {fmt.Println(r)}}
Line-by-line code explanation
var wg sync.WaitGroupdeclares a counter-based synchronization primitive.wg.Add(1)increments the counter before launching each goroutine — call from the parent, not inside the child.go func() { defer wg.Done(); ... }()decrements the counter when work finishes.wg.Wait()blocks until the counter reaches zero.defer wg.Done()ensures Done runs even if the goroutine panics (but recover separately).Add must happen before Wait— race detector flags Add after Wait starts.copy loop variable— pass parameters into the goroutine closure to avoid capture bugs.WaitGroup vs errgroup— errgroup (x/sync) also collects the first error from workers.
Key takeaway: Add(1) per goroutine before go statement. Separate goroutine closes results after Wait. Pass pointer to WaitGroup.
Real-world use
Where you'll use this in production
- Parallel test setup/teardown in integration tests.
- Batch HTTP health checks across microservices.
- File processing workers on directory of files.
- Load test coordination firing concurrent requests.
Best practices
- Call Add before go — never inside goroutine unless careful.
- Always defer Done at goroutine entry.
- Use channel or mutex for collecting results, WaitGroup only for count.
- Consider errgroup when first error should cancel siblings.
- Enable race detector in CI: go test -race.
Common mistakes
- wg.Add inside goroutine racing with Wait.
- Forgetting Done — Wait blocks forever.
- Calling Add with wrong count.
- Reusing WaitGroup before previous Wait completes.
- Sharing WaitGroup value instead of pointer incorrectly.
Advanced interview questions
Q1BeginnerWaitGroup Add/Done/Wait?
Q2BeginnerWaitGroup vs channel for completion?
Q3IntermediateRace on WaitGroup Add?
Q4Intermediateerrgroup vs WaitGroup?
Q5AdvancedParallel fetch with error handling design.
Summary
WaitGroup coordinates goroutine completion counting. Add before launch, defer Done in worker, Wait in coordinator. Use errgroup when errors should cancel parallel work. WaitGroup signals completion; use channels for results. Next lesson: Mutex — protecting shared state.