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

RuleReason
Call wg.Add(1) before starting the goroutineAvoid 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 useCopying 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 main until all goroutines call Done()
  • 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.Sleep as the proper synchronization tool

Leave a Comment