Go Test Panic: test timed out — Goroutine Deadlock
Diagnose "panic: test timed out" by reading the goroutine dump Go prints, finding the blocked channels or locks, and adding timeouts to waits.
Symptoms
- go test fails with "panic: test timed out after 10m0s"
- The stack dump lists goroutines all in chan receive or sync.Mutex
- Removing one send makes the test pass
Possible causes
- A channel send has no corresponding receive, blocking the sender forever
- Two goroutines each wait for the other to release a lock
- A WaitGroup counter never reaches zero because a goroutine never starts or panics
Diagnosis steps
Run with a short timeout to capture the dump quickly
go test -run TestX -timeout 15sInterpretation
The crash dump shows every goroutine and the exact line each is blocked on, naming the culprit.
Find the blocking primitive in the dump
go test -run TestX -timeout 15s 2>&1 | grep -A 8 "goroutine .* \["Interpretation
Grouping goroutines by state shows whether it is a send/receive mismatch or a lock cycle.
Root cause
Go blocks instead of failing when every goroutine is waiting with no possible progress. A goroutine dump then shows all goroutines parked in chan operations or mutex waits, which classifies the hang as a deadlock or a missing receiver.
Fix
Balance sends and receives, buffer the channel where appropriate, give locks a consistent acquisition order, and always make waits selectable with a timeout or context cancellation.
Risk note
Adding a timeout can hide a genuine deadlock; fix the design rather than only capping the wait.
Verify the fix
Rerun the test under -timeout and confirm it completes well inside the limit. Run with -race and -count=100 to catch intermittent hangs.
Symptoms
This guide profiles the failure behind “Go Test Panic: test timed out — Goroutine Deadlock”. 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:
- go test fails with “panic: test timed out after 10m0s”
- The stack dump lists goroutines all in chan receive or sync.Mutex
- Removing one send makes the test pass
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: Run with a short timeout to capture the dump quickly
go test -run TestX -timeout 15s
The crash dump shows every goroutine and the exact line each is blocked on, naming the culprit.
Step 2: Find the blocking primitive in the dump
go test -run TestX -timeout 15s 2>&1 | grep -A 8 "goroutine .* \["
Grouping goroutines by state shows whether it is a send/receive mismatch or a lock cycle.
Likely contributors, in order of frequency:
- A channel send has no corresponding receive, blocking the sender forever
- Two goroutines each wait for the other to release a lock
- A WaitGroup counter never reaches zero because a goroutine never starts or panics
Root Cause
Go blocks instead of failing when every goroutine is waiting with no possible progress. A goroutine dump then shows all goroutines parked in chan operations or mutex waits, which classifies the hang as a deadlock or a missing receiver.
The Fix
Balance sends and receives, buffer the channel where appropriate, give locks a consistent acquisition order, and always make waits selectable with a timeout or context cancellation.
Watch out: Adding a timeout can hide a genuine deadlock; fix the design rather than only capping the wait.
// ❌ unbuffered send with no reader -> deadlock
ch := make(chan int)
ch <- 1 // blocks forever
// ✅ buffered, then read
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
// ✅ wait with timeout
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for worker")
}
Verify and Prevent
Rerun the test under -timeout and confirm it completes well inside the limit. Run with -race and -count=100 to catch intermittent hangs.
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.