Menu

Golang WaitGroup: Add, Done, Wait and a Worker Pool

How sync.WaitGroup waits for a set of goroutines to finish: the Add, Done and Wait rules, why it must be passed by pointer, collecting results and errors, and a worker pool built on it.

This page includes runnable editors - edit, run, and see output instantly.

The basic pattern

sync.WaitGroup counts running goroutines. Add raises the count, Done lowers it, Wait blocks until it is zero.

The three downloads run concurrently, so the program takes about 10 ms instead of 30. The results are printed in input order because each goroutine writes only its own index of sizes, and main reads them only after Wait.

The zero value of a WaitGroup is ready to use. No constructor.

The three rules

Call Add before go, not inside the goroutine. If the goroutine calls Add itself, main can reach Wait before any goroutine has started, see a count of zero, and return while the work has not begun. When you know the count in advance, wg.Add(len(files)) once before the loop is equivalent.

Call Done with defer as the first line of the goroutine. A goroutine that returns early on an error, or panics, still decrements the counter. A missing Done leaves Wait blocked forever. If it is the only goroutine left, the runtime reports fatal error: all goroutines are asleep with sync.WaitGroup.Wait in the trace.

Never copy a WaitGroup after first use. Pass *sync.WaitGroup to functions, or capture the variable in a closure as above.

Passing a WaitGroup to a function

When the goroutine body is a named function, pass a pointer:

With wg sync.WaitGroup as a value parameter, each worker would call Done on its own copy and main would block in Wait forever. go vet catches it before you run anything:

./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy

A cleaner design keeps concurrency out of worker entirely: let it be a plain function and do the Add/Done bookkeeping in the caller's closure. Then worker is easy to test and call synchronously.

Negative counter

Done is Add(-1). If the count goes below zero, the program panics:

The output is recovered: sync: negative WaitGroup counter. The usual cause is a goroutine with defer wg.Done() that also calls wg.Done() explicitly on some path.

Collecting errors

A WaitGroup only counts. For errors, give each goroutine its own slot and inspect them after Wait:

errors.Join (Go 1.20) skips nil values and returns nil if all of them are nil, so it combines "one error per goroutine" without any extra bookkeeping.

If you want to stop the remaining work as soon as one goroutine fails, use golang.org/x/sync/errgroup instead. It is a WaitGroup plus the first error plus a context that is cancelled on failure, and g.SetLimit(n) caps concurrency. It lives outside the standard library, so it cannot run in this page's editor:

g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
	g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
	return err // the first error; ctx was cancelled for the others
}

A worker pool

A fixed number of goroutines reading jobs from a channel keeps concurrency bounded no matter how many jobs there are. The WaitGroup tells you when all workers are done, which is when the results channel can be closed.

The ordering of the three pieces matters:

  • main must be receiving results while the workers run. If main called wg.Wait() directly before reading, the workers would block sending to results, never reach Done, and everything would deadlock. That is why Wait runs in its own goroutine.
  • close(results) happens only after Wait, so no worker can send on a closed channel.
  • The job feeder also runs in a goroutine, so feeding and collecting overlap.

Which worker handled which job changes from run to run, so the program sorts by job before printing. Everything it prints is deterministic.

WaitGroup, channel, or errgroup

NeedUse
Wait for N goroutines, results in indexed slotssync.WaitGroup
Wait for one goroutinea done channel or the result channel itself
Results streamed as they finisha channel, closed after wg.Wait()
Stop everything on the first errorerrgroup.WithContext
Stop everything on a timeout or caller cancelcontext.Context plus a WaitGroup or errgroup

Go 1.25 adds wg.Go(func() { ... }), which does the Add(1) and the deferred Done for you. Code for Go 1.24 and earlier, including this page's editor, uses the explicit form shown above.

Common mistakes

  • wg.Add(1) inside the goroutine. Wait can return before it runs.
  • Forgetting Done on an early return. Always defer wg.Done().
  • Passing the WaitGroup by value. Use a pointer; go vet flags the copy.
  • Waiting in the same goroutine that must drain a channel. Move wg.Wait() plus close into a separate goroutine.
  • Reusing a WaitGroup before the previous Wait has returned. Start a new cycle of Add calls only after Wait is done.

Frequently Asked Questions

How does sync.WaitGroup work in Go?

A WaitGroup is a counter. wg.Add(n) increases it, wg.Done() decreases it by one, and wg.Wait() blocks until it reaches zero. Call Add before starting each goroutine, defer wg.Done() inside it, and Wait where you need everything finished.

Should I pass a WaitGroup by value or by pointer?

By pointer (*sync.WaitGroup), or let goroutines capture it in a closure. A copy has its own counter, so Done on the copy never reaches the original and Wait blocks forever. go vet reports the mistake as "passes lock by value".

What causes "sync: negative WaitGroup counter"?

More Done calls than Add calls. Usually a goroutine calls Done twice (once with defer and once explicitly), or Add(1) is skipped on one path. The program panics, since the counter can no longer tell you anything true.

How do I get errors from goroutines started with a WaitGroup?

A WaitGroup carries no results or errors. Give each goroutine its own slot in a slice of errors and combine them after Wait (for example with errors.Join), or use golang.org/x/sync/errgroup, whose Wait returns the first error and can cancel the others through a context.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED