You range over a channel by using the for range loop, which automatically receives values until the channel is closed. The loop terminates immediately when the channel closes, making it the idiomatic way to consume a stream of data without manual select statements or explicit close checks.
Here is a practical example of a producer goroutine sending data and a main function consuming it via range:
package main
import (
"fmt"
"time"
)
func main() {
// Create an unbuffered channel
nums := make(chan int)
// Start a goroutine to send data
go func() {
for i := 1; i <= 3; i++ {
nums <- i
time.Sleep(100 * time.Millisecond)
}
close(nums) // Crucial: signal that no more data is coming
}()
// Range over the channel to receive values
for n := range nums {
fmt.Println("Received:", n)
}
fmt.Println("Channel closed, loop finished.")
}
If you need to handle a scenario where the channel might not be closed (e.g., waiting for a timeout or a specific signal), you cannot rely solely on range. In those cases, use a select statement with a case for the channel and a case for a timeout or a done channel. However, for standard producer-consumer patterns where the producer knows when it is finished, range is the cleanest approach.
Remember that if you range over a channel that is never closed, the loop will block indefinitely waiting for the next value. Always ensure the sender calls close() on the channel when finished, or use a select with a timeout to prevent deadlocks in long-running services.
// Example: Handling a channel that might not be closed immediately
func safeReceive(ch <-chan int) {
select {
case val, ok := <-ch:
if !ok {
fmt.Println("Channel closed")
return
}
fmt.Println("Got:", val)
case <-time.After(2 * time.Second):
fmt.Println("Timeout waiting for data")
}
}
Using range simplifies your code significantly by removing the need for explicit ok checks inside the loop body, as the loop condition handles the closed state automatically.