gonjure
A grimoire of go incantations

← back to the grimoire

WaitGroup

Ready your lightning bolts. Leave not the tower sanctity until enemies are vanquished.

grimoire fragment · main.go
package main

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

func main() {
  var (
	  imps = summon(4)
    tally  int
    wg     sync.WaitGroup
  )

	for _, imp := range imps {
		wg.Add(1)

    tally++

		go vanquish(&wg, imp, 2000)
	}

	wg.Wait() // remain atop spire.
	
  fmt.Printf("spire stands, %d imps vanquished\n", tally)
}

func summon(count int) []string {
	imps := make([]string, count)

	for imp := range imps {
		imps[imp] = fmt.Sprintf("imp-%d", imp+1)
	}

	return imps
}

// sleep, smite, report.
func vanquish(wg *sync.WaitGroup, name string, delay int) {
	defer wg.Done()

	time.Sleep(time.Duration(delay) * time.Millisecond)

	fmt.Println("vanquished", name)
}
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 until the tally has fallen back to zero. It is a simple and sturdy way to not budge until all the work is done.

Why a wizard cares

When main returns, the program simply exits and every goroutine it spawned is discarded mid incantation. If your wizard (main) wanders off the spire while work (goroutines) is still in flight then the work vanishes with him.

Try it! With waitgroup on, the wizard stays at his post and every imp is dispatched in turn. Only once the count is at zero does he fade from the parapet and rematerialise at the door. Turn the toggle off and watch the same opening play out differently. With no wg.Wait() to hold him in place, the wizard fades from the parapet at once and reappears at the door. Only afterwards when already at ground level and out of the fight, do a pair of lightning strikes drop from the empty sky and smite two of the imps in the field. Those late bolts are the goroutines racing on past main. They may complete but main has long since completed. A third unstruck imp marches all the way across the ground and reaches the wizard, where a puff of purple smoke marks the moment main is overrun.

Shape of the spell

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

  • Add. 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 as a value.

Pitfalls

  • Negative counter will panic. Calling Done() more times than Add() drives the counter below zero and the runtime ends the show with a panic. Match them exactly.
  • Does not collect results or errors. A WaitGroup only waits. For "wait and propagate the first error", reach for golang.org/x/sync/errgroup.
  • No cancellation. 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.

Reagents required: sync, already in your standard pouch.