Range Over Channel
Fast recall
Quick revision
Core answer
Whether you understand Range Over Channel beyond memorized syntax.
Giving a definition without explaining a practical trade-off.
Connect the concept to a small production example and state its complexity or failure mode.
Interview-ready guide
Interview answer
range over a channel is a receive loop the compiler writes for you:
for v := range ch {
process(v)
}Each iteration is a receive; the loop ends when the channel delivers the end-signal — closed and drained. There is NO other exit: no condition clause, no break-on-error, nothing to configure. The loop either runs until close or blocks forever on an empty open channel.
That's why range over channels requires the close discipline: the producer MUST close (typically defer close at the top of the producing function), or every downstream range leaks into a permanent block. The pairing is the idiom:
func produce(out chan<- int) {
defer close(out) // the range's termination signal
for _, v := range data {
out <- v
}
}
// consumer
for v := range out { // terminates exactly when produce returns
use(v)
}The mechanics under the loop: range is the comma-ok receive — each iteration does v, ok := <-ch; ok true → body; ok false → exit. Buffered values drain normally (ok true for each); the close is seen only when the buffer empties. So range gets the lossless-drain-then-terminate guarantee the close semantics provide.
Where range DOESN'T fit: when the consumer has multiple channels, needs timeouts, or wants to stop early — those are select's jobs, with the comma-ok form explicitly written. range is the single-channel, run-to-completion loop — the simplest correct consumer, provided the producer owns the close.
Range Over Channel
Direct Answer
for v := range ch receives until the channel is closed AND drained, then terminates. Without a close, an empty channel blocks the range forever — range has no other exit.
Deep Dive & Architecture
The mechanics under the loop, once, because everything about range-over-channel follows from it: each iteration is a receive. The receive blocks until a value arrives (or the channel is closed-and-drained). ok=true → body runs with the value. ok=false → loop exits. Range doesn't 'know' about close as a special event — it sees exactly what a comma-ok receive sees: values, then the end-signal.
Which means range's termination depends ENTIRELY on the close discipline: an open channel that goes quiet just blocks the range at its next receive — the goroutine parks, forever, in the channel's wait queue. The range is patient to a fault; it will out-wait the program.
The pairing idiom that makes this safe — producer owns the close, defer at the top:
func produce(out chan<- int) {
defer close(out)
// any number of sends, any exit path
}
Because the close is deferred, it fires on EVERY return — early exit, error return, panic-recover, natural completion. The downstream range terminates in all cases. Without the defer (close at the 'end'), an early return path skips it — the range hangs — and that bug is invisible in tests (the happy path closes) and fatal in production (the error path hangs the consumer).
Where range is the wrong tool: any exit condition OTHER than the channel's end. Early termination (break), multiple channels (select), timeouts (select+time.After), per-receive branching beyond processing — the explicit for+comma-ok loop states those visibly. Range is the one-shape consumer: single channel, run-to-completion, trust the producer's close.
Common Mistakes & Pitfalls
Giving a definition without explaining a practical trade-off.
Check your understanding
Answer these without looking back at the article.
- When would you use Range Over Channel in a real project?
- How would you compare Range Over Channel with an alternative?
- What is a common mistake when using Range Over Channel?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.