debug a problem involving Range Over Channel
Fast recall
Quick revision
Core answer
Practical understanding of Range Over Channel.
Repeating a definition without saying when to use it.
Give one trade-off and say how you would test it.
Interview-ready guide
Interview answer
range over a nil channel is the nil-receive behavior on loop:
var ch chan int // nil
for v := range ch { // first receive: block forever
process(v) // never runs
}The goroutine parks on the first iteration's receive and never wakes — a leak with a stack trace showing the park. If every other goroutine also blocks, the runtime's deadlock detector fires; if anything else runs, the leak is silent.
The classic cause: a channel field or variable declared but never make'd, passed into a range-driven consumer. The compiler can't catch it (the nil channel is a runtime state), so it surfaces as 'this worker never processes anything' — and the diagnostic is the goroutine dump: the parked receive shows the nil channel in its frame.
One legitimate use, inherited from the nil-receive rules: a range over a channel variable that BECOMES nil mid-loop (set to nil elsewhere, race aside) would block at the next receive — the loop can't be disabled that way cleanly. The select-based nil trick (nil disables a select CASE) doesn't transfer to range; range is single-channel and total. If you need 'stop consuming this source temporarily', you're outside range's design — select with the channel plus a control channel is the shape.
debug a problem involving Range Over Channel
Direct Answer
A nil channel in a range blocks forever on the first receive — same as any nil receive. range never sees values or termination; the goroutine parks permanently.
Deep Dive & Architecture
The nil-channel rules, applied to range: a receive on nil blocks forever — range's FIRST receive blocks forever — the goroutine parks in the nil channel's (nonexistent) wait queue, permanently. The loop body never runs; there's no iteration count, no timeout, no error — just a parked goroutine with a stack showing the receive.
The diagnostic: runtime goroutine dump (pprof's goroutine profile, or SIGQUIT's dump) shows the goroutine blocked on 'chan receive' with the channel argument nil — the stack trace's frame shows the nil. That's the one way this bug identifies itself: it never self-heals, never crashes (unless it's the last runnable goroutine — then deadlock detector), just leaks.
How the nil reaches the range — the real-world patterns:
- Declared, never made: var results chan Result; go produce(results); for r := range results — producer blocks on its nil send, consumer blocks on its nil receive: two leaks, mutually invisible.
- A channel field in a struct not initialized by a constructor that half the code paths use: works in tests (the constructor path), hangs in production (the other path).
- A nil channel deliberately passed (select-disabled idiom) that someone ranges instead of selects: the trick doesn't transfer — nil disables a select CASE; it permanently parks a range.
The fix is the discipline the channel-creation question established: channels are born from make — at declaration, in a constructor, or immediately before use — and a struct holding a channel initializes it in ONE place every path passes through. A channel field that can be nil is a struct with an unset lifecycle: finish the construction.
Common Mistakes & Pitfalls
Repeating a definition without saying when to use it.
Check your understanding
Answer these without looking back at the article.
- What is Range Over Channel, and when would you use it?
- When would you use Range Over Channel in a real project?
- How would you compare Range Over Channel with an alternative?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.