Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 25

    WaitGroup

    sync.WaitGroup waits for a collection of goroutines to finish.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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

    1. Counter increments with Add, decrements with Done.
    2. Wait blocks while counter > 0.
    3. Each goroutine calls Done exactly once (defer ensures this).
    4. Pass &wg to goroutine or capture in closure.
    5. Wait after all workers launched — main coordination point.
    6. Combine with channels for results after Wait completes.

    Practical code example

    Parallel URL health checks with WaitGroup:

    go
    package main
    import (
    "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.WaitGroup
    results := 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.WaitGroup declares 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?
    Add increments wait counter; Done decrements; Wait blocks until zero.
    Q2BeginnerWaitGroup vs channel for completion?
    WaitGroup for count-only; channel when passing results or cancel signals.
    Q3IntermediateRace on WaitGroup Add?
    Add must complete before Wait and before goroutine reads counter — call before go.
    Q4Intermediateerrgroup vs WaitGroup?
    errgroup propagates first error and cancels context for sibling goroutines.
    Q5AdvancedParallel fetch with error handling design.
    errgroup.WithContext; each goroutine returns error; g.Wait() returns first error; context cancels rest.

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.