lang-wiz
a grimoire of advanced Go incantations · turn the page slowly

← back to the grimoire

WaitGroup

Defend the spire; do not leave until it is safe. Tally each apprentice, and wait for every last one to come home.

grimoire fragment · main.go
package main

import (
	"fmt"
	"sync"
	"time"
)

// vanquish: one apprentice's whole working day. Sleep, smite, report.
func vanquish(name string, spellMs int, wg *sync.WaitGroup) {
	defer wg.Done() // the apprentice's hand on the rune as he returns home
	time.Sleep(time.Duration(spellMs) * time.Millisecond)
	fmt.Println("vanquished:", name)
}

func main() {
	var wg sync.WaitGroup
	imps := summonImps(4)
	for _, imp := range imps {
		wg.Add(1) // tally an apprentice before dispatching
		go vanquish(imp, 2000, &wg)
	}
	wg.Wait() // stay atop the spire until every apprentice has returned
	fmt.Printf("the spire stands; %d imps vanquished\n", 4)
}
Defence of the spire
wg wg.Wait() — the wizard fades through the door only once the field is clear
Dials & sigils

What you are witnessing

A sync.WaitGroup is a counter with two ceremonies: Add(n) raises a tally for goroutines about to begin, and Done() strikes one off when each is finished. Wait() blocks the calling goroutine until the tally has fallen back to zero. It is the simplest, sturdiest way for one goroutine to say I will not budge until you are all home.

Why a wizard cares

A Go program ends when main returns. There is no quiet "let the other spells finish" — the program simply exits, and every goroutine it spawned is discarded mid-incantation. If your wizard (main) wanders off the spire while apprentices (goroutines) are still in flight, their work vanishes with him. Print statements never reach the parchment. Files are written halfway. Imps remain, unvanquished.

Try it: with waitgroup on, the wizard stays at his post and every imp is dispatched in turn, one tally tick at a time; only once the count is back at zero does he fade from the parapet and rematerialise at the door, free to walk out into the day. Turn the toggle off and watch the same opening play out — the wizard despatches an imp or two and then, with no wg.Wait() to hold him in place, fades to the door far too early. An imp is still abroad; he marches right up to the unguarded door and catches the wizard there in a puff of smoke. The count of "imps vanquished" in the code drops to 0 because, from main's point of view, none of those goroutines were ever observed to finish.

The shape of the spell

The canonical rhythm is Add before you go, Done as you return, Wait when you must:

  • Add on the casting side. Call wg.Add(1) before the go statement, not inside the goroutine. If you call it inside, Wait may run before Add, and you'll find the tally already at zero.
  • Done with defer. defer wg.Done() at the top of the goroutine survives panics, early returns, and your own forgetfulness.
  • Pass by pointer. A WaitGroup must not be copied after first use. Hand goroutines a *sync.WaitGroup, never a value.

Pitfalls (the asterisks every wizard must read)

  • Negative counter panics. Calling Done() more times than Add() drives the counter below zero and the runtime ends the show with a panic. Match them exactly.
  • It does not collect results or errors. A WaitGroup only waits. For "wait and propagate the first error", reach for golang.org/x/sync/errgroup.
  • It does not cancel anything. Slow apprentices keep working until they finish. Pair with a context.Context when you need a leash.
  • Don't reuse without care. A WaitGroup may be reused once Wait returns, but only if you're certain no apprentice from the previous round is still about to call Done. When in doubt, allocate a fresh one.
  • Wait() is not a deadline. If even one apprentice never calls Done, the wizard waits forever. Combine with timeouts or contexts in long-lived programs.

Reagents required: sync, already in your standard pouch. No newt eyes.