Go — sync.WaitGroup deadlock or negative counter panic
Fix WaitGroup misuse by adding before goroutines start and passing counters by pointer or closure.
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.goInterpretation
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.goInterpretation
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 ./...