How does for range over a channel work, and what are its pitfalls?
Question 196MediumGo 1.22 to 1.25
for v := range ch receives until the channel is closed and drained, then exits. It reads one value per iteration (there is no index variable), and ranging over a nil channel blocks forever.
Pitfalls:
- Nobody closes the channel: the loop never ends, and you get a leak or deadlock. The producer must close it when it's done.
- No cancellation:
rangecan't watchctx.Done(). If the producer stalls, the consumer is stuck. - Breaking out early leaks the producer: if the consumer
breaks and the producer is blocked sending on an unbuffered channel, the producer goroutine blocks forever.
func gen(ctx context.Context) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for i := 0; ; i++ {
select {
case out <- i:
case <-ctx.Done(): // lets the consumer abandon us safely
return
}
}
}()
return out
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for v := range gen(ctx) {
if v == 5 { break } // cancel() via defer unblocks the producer
}
Since Go 1.23, a range-over-func iterator (iter.Seq) is often the better API for pure sequences. It needs no goroutine, and break can't leak one.
More on Channels & select
- Q194What causes "fatal error: all goroutines are asleep - deadlock!" and when does Go fail to detect a deadlock?
- Q195What does this program do? (unbuffered send in main)
- Q197What are directional channel types and why use them?
- Q198How do you implement a timeout on a channel operation? Compare
time.After,time.NewTimer, andcontext. - Q199Does
time.Afterin a loop leak memory? How did Go 1.23 change this? - Q200What's the difference between
time.Tickerand repeatedtime.After? What happens if the consumer is slow?