Introduction
Go's for range loop works with channels, providing a clean way to receive all values until the channel is closed. This pattern is the backbone of producer-consumer designs in Go.
Key Concepts
- Range Over Channel:
for v := range chreceives values fromchuntil it is closed and drained. - Channel Close as Signal: Closing a channel tells all receivers that no more values will come.
- Deadlock on Unclosed Channel: If the channel is never closed, the range loop blocks forever.
Real World Context
In a log processing pipeline, a producer goroutine reads log lines from a file and sends them to a channel. The consumer uses for line := range logCh to process each line. When the file is fully read, the producer closes the channel, and the consumer exits its loop cleanly.
Deep Dive
You can use for range to iterate over values received from a channel. The loop terminates when the channel is closed and all buffered values are drained:
goch := make(chan int, 2) ch <- 1 ch <- 2 close(ch) for v := range ch { fmt.Println(v) // Prints 1, then 2, then exits }
Producer Pattern
gofunc generate(nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) // Always close! for _, n := range nums { out <- n } }() return out }
The defer close(out) ensures the channel is closed even if the goroutine exits early due to a panic. Consumers using for range will receive all values and then exit the loop.
Common Pitfalls
- Forgetting to close the channel — If the producer never closes the channel, the consumer's
for rangeloop blocks forever (deadlock or goroutine leak). - Closing the channel before all values are sent — This causes a panic on the next send. Always send all values first, then close.
Best Practices
- Use
defer close(ch)in the producer goroutine — This guarantees the channel is closed even if the goroutine panics. - Return receive-only channels from producers — The generator pattern (
func gen() <-chan T) encapsulates the goroutine and channel lifecycle.
Summary
for v := range chreceives until the channel is closed and drained.- Always close the channel from the sender side using
defer close(ch). - An unclosed channel causes the range loop to block forever.
- The generator pattern returns a receive-only channel and manages its lifecycle internally.
Code Examples
func producer(out chan<- int) {
for i := 0; i < 5; i++ {
out <- i
}
close(out) // Essential: signals consumers to stop
}
func main() {
ch := make(chan int)
go producer(ch)
for n := range ch {
fmt.Println(n) // Prints 0, 1, 2, 3, 4 then exits
}
}