A goroutine ranges over a channel the producer never closes — what happens to that goroutine?
answer
- only one thing ends that loop
- empty is not the same as closed
- the producer returned early on the error path
- put it in a defer, not after the loop
- and only the sender may do it
basics
~10 sA range over a channel ends only when the channel is closed, so the consumer parks on the receive forever. An empty channel, or a producer that has returned, does not end the loop.
solid answer
~50 s`for v := range ch` receives until the channel is closed, and closing is the *only* thing that ends it. If the producer returns without closing — very often on an error path, where the `close` sits after the loop that returned early — the consumer receives nothing more and blocks on the receive indefinitely. It is not an error the runtime reports; the consumer is simply one more parked goroutine. The rule is that the sending side closes, exactly once, on every return path, which is what `defer close(ch)` at the top of the producer buys you. Note the asymmetry: `close` is safe and useful for waking receivers, but closing from the receiving side risks a `send on closed channel` panic in the producer, and closing an already-closed channel panics too. If several producers feed one channel, none of them may close it individually; a coordinator closes after they have all finished.
code
go · 11 linesfunc produce(out chan<- []byte, src io.Reader) error {
sc := bufio.NewScanner(src)
for sc.Scan() {
out <- sc.Bytes()
}
if err := sc.Err(); err != nil {
return err // close(out) below never runs
}
close(out)
return nil
}go deeper
Remember that a range over a channel ends only when the channel is closed, and that an empty channel simply blocks. Know that the sending side is the side that closes.
Explain the desugaring into a receive with the comma-ok form and why neither emptiness nor the producer returning ends the loop. Be ready to say why defer close is the idiom and why closing twice or closing from a receiver panics.
Show that you look for the return path where the close is skipped, usually the error branch, and that you can explain why the leak rate then tracks the failure rate rather than the traffic rate. Know how to close correctly when several producers share one channel.
Set the convention that the goroutine owning the sending side closes in a defer, and that fan-in channels get a single coordinator responsible for the close. Decide how much of this a review checklist enforces versus a pipeline helper others reuse.
## What `range` over a channel actually does `for v := range ch` is defined as: receive from `ch`; if the receive yields a value with `ok` true, run the body; if the channel is closed and drained, end the loop. Written out, it is: ```go for { v, ok := <-ch if !ok { break } // body } ``` The consequence people miss is that **nothing else ends this loop**. Not the channel being empty — an empty open channel just blocks the receive. Not the producer goroutine returning — a channel has no notion of who its senders are and no reference count. Not the channel becoming unreachable from other code. Only `close(ch)`. So a consumer whose producer forgot to close is parked in the receive, and stays parked for the life of the process, holding its stack and every value it still references. ## Where the missing close actually comes from Almost never from someone forgetting entirely. The `close` is usually there and is simply not on every path: ```go func produce(out chan<- []byte, src *os.File) error { sc := bufio.NewScanner(src) for sc.Scan() { out <- sc.Bytes() } if err := sc.Err(); err != nil { return err // the close below never runs } close(out) return nil } ``` The happy path closes; the error path returns and leaves every consumer ranging over `out` stranded. Moving it to `defer close(out)` on the first line of the producer fixes every present and future return path at once, which is why that idiom is the convention. ## The rules around closing - **The sending side closes.** A receiver has no way to know whether a send is in flight, and closing under a blocked sender panics with `send on closed channel`. - **Close exactly once.** Closing an already-closed channel panics, and so does closing a nil channel. - **Closing is a broadcast.** Every goroutine blocked receiving wakes at once and gets the zero value with `ok` false, which is why closed channels are also used as a done signal. - **You do not have to close.** A channel that is simply dropped is garbage-collected like any other value. Closing exists to *tell receivers there is no more data*; if nothing ranges over it, nothing needs the signal. - **Multiple producers cannot each close.** The second `close` panics. Have the producers signal completion — a `sync.WaitGroup` joined by a small coordinator goroutine that does the single `close` — so exactly one close happens after the last send. ## Why it is invisible A consumer parked in `chanrecv` costs no CPU and raises no error. The runtime's `all goroutines are asleep - deadlock!` abort requires *every* goroutine to be blocked, which is never true in a service that is still accepting work. In a long-running worker that spins up a consumer per batch, the count grows by one per batch that hit the error path — an accumulation rate tied to your error rate, which is exactly why it usually starts on the day a downstream system gets flaky. ## Reading code for it For every `for range ch`, find the `close(ch)` and then ask which return paths of the producer reach it. If the answer is not "all of them", it leaks on the others. `defer close(ch)` immediately after the goroutine that owns the sending side starts is the shape that makes the answer "all of them" by construction.
- Does a range over a channel end when the goroutine that was sending to it returns?No. A channel does not track its senders and has no reference count, so a producer returning changes nothing about the channel's state. The consumer's receive simply blocks on an open, empty channel until someone calls `close`. That gap between "the sender is gone" and "the channel is closed" is where this leak lives.
- Four producer goroutines feed one channel. Who closes it?None of them individually — the second close would panic. Have each producer signal completion, typically through a `sync.WaitGroup`, and start one small coordinator goroutine that waits for all of them and then performs the single `close`. That guarantees exactly one close, after the last send has happened.
- Is it a leak to drop a channel without ever closing it?Not by itself. An unreferenced channel is garbage-collected like any other value; `close` exists to tell receivers that no more data is coming, not to free memory. It becomes a leak only when a goroutine is blocked receiving on it, because that goroutine is a GC root and will never resume.
saying these in an interview costs you the question
- Thinks a range over a channel ends when the channel is empty
- Believes the loop ends when the producer goroutine returns
- Has the consumer close the channel it reads from
- Lets each of several producers close the shared channel
- Claims every channel must be closed or it leaks memory