The rule, and the reason behind it
The sending side closes a channel; the receiving side never does. This is
not a convention, it follows from what the operations do. A receive on a closed
channel is well defined — it yields the zero value immediately, forever. A send
on a closed channel is a panic, and there is no way to test for it first,
because any check would race with another goroutine closing between the check
and the send.
So closing is a claim that no further sends will happen, and only the senders
are in a position to make that claim.
With one producer this is trivial: it closes when it finishes. With several, no
individual producer can close, because the others may still be running. The
information needed — all producers have finished — does not exist inside any
one of them.
The standard shape
Create the fact somewhere it can exist, which means a WaitGroup the producers
signal and a separate goroutine that waits and then closes.
func fanIn(ctx context.Context, sources []Source) <-chan Record {
out := make(chan Record)
var wg sync.WaitGroup
for _, src := range sources {
wg.Add(1)
go func(s Source) {
defer wg.Done()
for _, rec := range s.Read() {
select {
case out <- rec:
case <-ctx.Done():
return
}
}
}(src)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Three details carry the design.
wg.Add(1) is outside the goroutine. Inside, it races with wg.Wait() — the
waiter can observe a counter of zero before a producer has incremented it, close
the channel, and then that producer panics on its first send.
defer wg.Done() rather than a call at the end, so an early return or a panic
still decrements. A producer that returns early without signalling leaves
Wait blocked forever, the channel never closes, and the consumer's range
never ends — a hang with no error anywhere.
The close is in its own goroutine rather than after the loop, because wg.Wait()
on the calling goroutine would block before fanIn could return the channel,
and nobody would ever be reading it.
Closing is what ends a range
The consumer side is then the part people already know, and the reason the close
matters:
for rec := range results {
process(rec)
}
Without the close this loop blocks forever once the producers stop. That is the
symptom the question is really about: a program that processes everything
correctly and then hangs at the end, with no error and no CPU use, because
nobody told the consumer there was nothing more coming.
The two-value form is the other half of the same fact:
rec, ok := <-results
Note and empty. A closed channel still yields buffered values before it starts
reporting closure, so closing does not discard anything already sent.
Going the other way
The mirror question — how the consumer tells the producers to stop — has a
different answer, and candidates often try to use the same tool for both.
You do not close the data channel to signal upstream, because that would be the
receiver closing and would panic any producer mid-send. You pass a context, or
a separate done channel, and every producer selects on it alongside its send.
That is what the select in the example above is for: without it, a producer
blocked on out <- rec with no reader left is a leaked goroutine holding
whatever it captured, for the life of the process.
This asymmetry is worth stating explicitly in an interview, because it explains
why cancellation is a separate channel rather than a reuse of the first: data
flows one way and the stop signal flows the other, and closing only ever works
in the direction the data travels.
When you do not need any of this
A useful counter-instinct, because a candidate who reaches for channels
reflexively is a recognisable pattern. If the producers are simply computing
values and nothing needs to stream, a WaitGroup writing into a preallocated
slice by index needs no channel, no close and no synchronisation at all, because
each goroutine owns one element. Channels earn their cost when work must flow as
it becomes available, or when ownership genuinely transfers between goroutines.
Closing means "no more sends", which is a promise only a sender can make — so
with several senders the close belongs to whatever waits for the last one.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).