← Debugging Guides

阅读中文版本

Go WaitGroup panic — Add Called After Wait / Negative Counter

Fix sync.WaitGroup misuse panics by calling Add before starting goroutines, never inside them, and rejecting a negative counter.

advanceddebugginggoconcurrencywaitgroup

Symptoms

  • panic: sync: WaitGroup is reused before previous Wait has returned
  • panic: sync: negative WaitGroup counter
  • The race detector reports a WaitGroup misuse warning

Possible causes

  • wg.Add is called inside the spawned goroutine instead of before it starts
  • wg.Done is called more times than wg.Add
  • The same WaitGroup is reused for a second batch before the first Wait returns

Diagnosis steps

Reproduce the misuse under the race detector

go test -race ./...

Interpretation

The race detector flags Add-after-Wait and concurrent Add/Wait, naming the exact lines.

Audit Add placement

grep -rn "wg.Add\|wg.Done" .

Interpretation

Every Add must execute before the goroutine it tracks is launched; Done belongs inside the goroutine.

Root cause

WaitGroup requires a positive Add before Wait and before the tracked goroutines start. Calling Add concurrently with Wait, or after the counter reaches zero, races the internal state and panics with reuse or negative-counter errors.

Fix

Call wg.Add(n) once, before launching the n goroutines, put defer wg.Done() at the top of each goroutine, and use a fresh WaitGroup per batch or errgroup instead of reusing one.

Risk note

Copying a WaitGroup by value is also a bug; always pass it by pointer or embed it in a struct.

Verify the fix

Rerun with -race and -count=100 and confirm no panic and no warnings; assert the number of completed jobs equals n.

Symptoms

This guide profiles the failure behind “Go WaitGroup panic — Add Called After Wait / Negative Counter”. It walks through the observable symptoms, a reproducible diagnosis path, the underlying root cause, the concrete fix, and how to verify the change and prevent a regression.

Observed symptoms include:

  • panic: sync: WaitGroup is reused before previous Wait has returned
  • panic: sync: negative WaitGroup counter
  • The race detector reports a WaitGroup misuse warning

Reproduce and Diagnose

Reproduce the failure in isolation before changing any code. Work through the checks below in order; each one narrows the search space.

Step 1: Reproduce the misuse under the race detector

go test -race ./...

The race detector flags Add-after-Wait and concurrent Add/Wait, naming the exact lines.

Step 2: Audit Add placement

grep -rn "wg.Add\|wg.Done" .

Every Add must execute before the goroutine it tracks is launched; Done belongs inside the goroutine.

Likely contributors, in order of frequency:

  • wg.Add is called inside the spawned goroutine instead of before it starts
  • wg.Done is called more times than wg.Add
  • The same WaitGroup is reused for a second batch before the first Wait returns

Root Cause

WaitGroup requires a positive Add before Wait and before the tracked goroutines start. Calling Add concurrently with Wait, or after the counter reaches zero, races the internal state and panics with reuse or negative-counter errors.

The Fix

Call wg.Add(n) once, before launching the n goroutines, put defer wg.Done() at the top of each goroutine, and use a fresh WaitGroup per batch or errgroup instead of reusing one.

Watch out: Copying a WaitGroup by value is also a bug; always pass it by pointer or embed it in a struct.

var wg sync.WaitGroup
wg.Add(len(urls)) // BEFORE launching
for _, u := range urls {
	u := u
	go func() {
		defer wg.Done() // inside the goroutine
		fetch(u)
	}()
}
wg.Wait()

// then a fresh WaitGroup for the next batch — never reuse a waited one.
wg = sync.WaitGroup{}

Verify and Prevent

Rerun with -race and -count=100 and confirm no panic and no warnings; assert the number of completed jobs equals n.

Once verified, add a regression test that reproduces the original symptom so the failure cannot silently return.

This class of failure is usually environmental rather than pure logic, so capture the exact command, the environment variables, and the working directory in your bug report. Reproduce it in the smallest possible script, then widen the scope only once the minimal case is understood. A short written checklist for this failure mode saves hours the next time it appears.