Go

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: range can't watch ctx.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

All 38 Channels & select questions