← Back to Go Course | Chapter 11: Concurrency | Lesson 5 of 8

sync.WaitGroup

A WaitGroup is like a checklist that counts how many workers are still busy, and it lets the boss wait until that count drops to zero before moving on.

Waiting for Goroutines to Finish

sync.WaitGroup lets the main goroutine wait until a group of other goroutines have all finished, replacing fragile approaches like time.Sleep with a precise, deterministic signal.

Example: Waiting for Goroutines to Finish

markup
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			fmt.Println("task", n, "complete")
		}(i)
	}
	wg.Wait()
	fmt.Println("all tasks finished")
}

Add Before, Done Inside

Add(1) should be called once for each goroutine right before it's launched, from the main goroutine -- calling it from inside the new goroutine risks Wait() returning before Add has even run.

Note: Always defer wg.Done() as the very first line inside the goroutine, so it runs even if the goroutine panics.

Example: Add Before, Done Inside

markup
package main

import (
	"fmt"
	"sync"
)

func process(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("processing item", id)
}

func main() {
	var wg sync.WaitGroup
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go process(i, &wg)
	}
	wg.Wait()
	fmt.Println("done")
}

Passing WaitGroup by Pointer

A sync.WaitGroup must never be copied after first use -- always pass it around as a pointer (*sync.WaitGroup), since copying it would give different parts of the code independent, inconsistent counters.

Example: Passing WaitGroup by Pointer

markup
package main

import (
	"fmt"
	"sync"
)

func worker(wg *sync.WaitGroup, results *[]int, mu *sync.Mutex, n int) {
	defer wg.Done()
	mu.Lock()
	*results = append(*results, n*n)
	mu.Unlock()
}

func main() {
	var wg sync.WaitGroup
	var mu sync.Mutex
	results := []int{}
	for i := 1; i <= 4; i++ {
		wg.Add(1)
		go worker(&wg, &results, &mu, i)
	}
	wg.Wait()
	fmt.Println(results)
}
Common Mistakes
  1. Calling wg.Add() inside the goroutine instead of before launching it, creating a race on the counter's initial value.
  2. Forgetting to call wg.Done() (often via defer) inside each goroutine, causing Wait() to block forever.
  3. Copying a sync.WaitGroup by value into a function instead of passing a pointer, which silently breaks its counting.
Chapter Summary
  • sync.WaitGroup tracks a count of outstanding goroutines to wait for.
  • Add(n) increases the counter, Done() decrements it, and Wait() blocks until it reaches zero.
  • Call Add() before launching each goroutine, not from inside it, to avoid a race.
  • Always pass a *sync.WaitGroup (or hold it by reference) -- copying it breaks the count.
🔒

Chapter Quiz — Complete all 8 topics to unlock

0/8 topics done

Complete these topics first:

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.