Go: send or receive on a nil channel blocks forever
Diagnose a goroutine that hangs because it sends to or receives from a nil channel, and why the deadlock detector sometimes stays silent.
Symptoms
- fatal error: all goroutines are asleep - deadlock!
- A worker never starts because it waits on a channel that was never made
- The program hangs with no output and no CPU usage
Possible causes
- A channel field is left nil and used before make() runs
- A nil channel is passed into a select, disabling that case silently
- An unbuffered channel send has no matching receiver
- The main function returns while a receiver still waits, hiding the hang
Diagnosis steps
Look for make(chan ...) vs zero-value channel use
Interpretation
A nil channel blocks all sends and receives permanently; confirm construction precedes first use.
Dump goroutine stacks when hung
kill -QUIT <pid> # or send SIGQUITInterpretation
The dump shows a goroutine parked on a channel op, naming the channel variable and the line.
Check select cases for nil channels
Interpretation
A nil case in select is never chosen, which can silently drop the intended branch rather than panic.
Root cause
A nil channel has no receiver buffer and never becomes ready: any send or receive on it blocks forever. Go only reports "all goroutines are asleep" when every goroutine is blocked, so a hang with another live goroutine (or a returned main) can occur without the deadlock message.
Fix
Initialise every channel before use with make(chan T) or make(chan T, n), and guard against nil by construction or a nil check. Never send on nil; if a select case must be disabled, pass nil deliberately but document it. Use timeouts or context to bound waits so a missed signal cannot hang forever.
Verify the fix
Run the program with the race detector and a timeout; the send/receive must complete, and killing with SIGQUIT no longer shows a goroutine parked on the nil channel.
Symptoms
A goroutine hangs with no progress and the process may print fatal error: all goroutines are asleep - deadlock! — or may hang silently when another goroutine is still alive. CPU usage is near zero because nothing is runnable.
Diagnosis
Confirm the channel is constructed before use, then dump stacks:
kill -QUIT $(pgrep myapp)
The dump shows a goroutine blocked on a chan send or receive at a specific line; trace that channel’s variable to its declaration.
Fix
Always make channels before use and bound waits:
type Worker struct {
jobs chan int
}
func NewWorker() *Worker {
return &Worker{jobs: make(chan int, 8)}
}
func (w *Worker) run(ctx context.Context) {
select {
case job := <-w.jobs:
_ = job
case <-ctx.Done():
return
}
}
Verification
Run with -race and a context timeout. The program finishes, and a SIGQUIT dump no longer shows any goroutine parked on the previously nil channel.