Introduction
sync.WaitGroup panics with negative WaitGroup counter when Done() is called more times than Add() incremented. This happens when the order of operations is wrong (Done called before Add completes), when goroutines call Done on error paths without corresponding Add, or when a WaitGroup is reused before all goroutines have finished. The panic is non-recoverable and crashes the program, making it critical to structure WaitGroup usage so that Add always happens before the goroutine starts and Done always happens exactly once per Add.
Symptoms
``` panic: sync: negative WaitGroup counter
goroutine 5 [running]: sync.(*WaitGroup).Add(0xc0000a8060, 0xffffffffffffffff) /usr/local/go/src/sync/waitgroup.go:75 +0x147 sync.(*WaitGroup).Done(...) ```
Or:
panic: sync: WaitGroup is reused before previous Wait has returnedCommon Causes
- Done called without Add: Error path calls Done but Add never happened
- Add inside goroutine: Goroutine starts, calls Done, then Add happens
- WaitGroup reused: WaitGroup not reset between batches
- Multiple Done calls: Done called twice for single Add
- Race between Add and Wait: Add called concurrently with Wait
- Conditional Done: Done called conditionally but Add always called
Step-by-Step Fix
Step 1: Always Add before starting goroutine
```go // WRONG: Add inside goroutine for i := 0; i < 10; i++ { go func() { wg.Add(1) // Race: goroutine might call Done before Add defer wg.Done() work() }() } wg.Wait()
// CORRECT: Add before goroutine for i := 0; i < 10; i++ { wg.Add(1) // Increment BEFORE launching goroutine go func() { defer wg.Done() // Guaranteed to run exactly once work() }() } wg.Wait() ```
Step 2: Handle error paths safely
```go func processItems(items []Item) error { var wg sync.WaitGroup errChan := make(chan error, len(items))
for _, item := range items { wg.Add(1) go func(item Item) { defer wg.Done() // Always called, even on error
if err := processItem(item); err != nil { errChan <- err return // Done still called by defer } errChan <- nil }(item) }
wg.Wait() close(errChan)
// Check for errors for err := range errChan { if err != nil { return err } } return nil } ```
Step 3: Safe worker pool with WaitGroup
```go type WorkerPool struct { wg sync.WaitGroup jobs chan Job }
func NewWorkerPool(size int) *WorkerPool { return &WorkerPool{ jobs: make(chan Job, size), } }
func (wp *WorkerPool) Start(workers int) { for i := 0; i < workers; i++ { wp.wg.Add(1) go func(id int) { defer wp.wg.Done() for job := range wp.jobs { job.Execute() } }(i) } }
func (wp *WorkerPool) Stop() { close(wp.jobs) // Signal workers to exit wp.wg.Wait() // Wait for all to finish } ```
Prevention
- Always call
wg.Add(1)beforego func(), never inside the goroutine - Use
defer wg.Done()as the first line in the goroutine body - Never call
Done()more than once perAdd(1)call - Do not reuse a WaitGroup until
Wait()has returned - Create a new WaitGroup for each batch of work
- Use buffered error channels to collect errors from goroutines
- Add
wg.Add(1)immediately before thegostatement to avoid race conditions