← Debugging Guides

阅读中文版本

Go — sync.WaitGroup deadlock or negative counter panic

Fix WaitGroup misuse by adding before goroutines start and passing counters by pointer or closure.

intermediatedebugginggoconcurrency

Symptoms

  • fatal error: all goroutines are asleep - deadlock!
  • panic: sync: negative WaitGroup counter
  • The program hangs and never returns
  • Wait returns before workers finish

Possible causes

  • wg.Add is called after wg.Wait or after the goroutine starts
  • The WaitGroup is copied by value so the count is lost
  • wg.Done is never called on an error path
  • Add and Wait run concurrently without ordering

Diagnosis steps

Enable the race detector to surface ordering issues

go run -race main.go

Interpretation

The race detector frequently reveals the concurrent Add/Wait misuse behind a deadlock.

Check Add placement

grep -n "wg.Add\|wg.Wait\|wg.Done" main.go

Interpretation

Add must happen before the goroutine that calls Done starts, and before Wait, otherwise the count can be zero at Wait time.

Check for copying

go vet ./...

Interpretation

go vet reports copying a WaitGroup by value, which loses the shared counter state.

Root cause

The WaitGroup counter did not reflect the started work: Add ran too late, the value was copied, or Done was skipped, so Wait either blocked forever or found a negative count.

Fix

Call Add before launching goroutines, pass the WaitGroup by pointer or capture it in a closure, and ensure Done runs on every path including errors, ideally with defer.

Verify the fix

Run the program with and without the race detector and confirm it completes without deadlock or panic, and confirm all workers finished by checking their outputs.

Symptom walkthrough

A program either deadlocks with all goroutines asleep or panics with sync: negative WaitGroup counter. Both point at misuse of the counter: adding after Wait or after a goroutine starts, copying the WaitGroup by value so each copy has its own counter, or skipping Done on an error path. The runtime message names the mechanism; the cause is ordering.

Fix in practice

Call Add before starting the goroutines it accounts for and before any Wait, pass the WaitGroup by pointer or capture it in a closure rather than copying it, and call Done on every exit path using defer. The race detector and go vet catch the common variants.

Verification

Run with and without the race detector and confirm the program completes without deadlock or panic, then confirm all workers produced their results. Completion with correct outputs proves the counter now reflects the work.

Diagnostic commands

Check the behavior with commands like these:

go run -race main.go
grep -n "wg.Add\|wg.Wait\|wg.Done" main.go
go vet ./...