You can use the range keyword to iterate over values received from a channel. This provides a clean way to process all values sent on a channel until it’s closed.
Basic Range Over Channel
package main
import "fmt"
func main() {
// Create and populate a channel
queue := make(chan string, 2)
queue <- "one"
queue <- "two"
close(queue)
// Iterate over channel values
for elem := range queue {
fmt.Println(elem)
}
}
Output:
The range loop continues receiving from the channel until the channel is closed and all buffered values are consumed.
How Range Works with Channels
- Receives a value from the channel
- Assigns the value to the loop variable
- Executes the loop body
- Repeats until the channel is closed and empty
- Exits when channel is closed and drained
Channel Must Be Closed
// BAD: Channel never closed - infinite loop or deadlock
ch := make(chan int, 3)
ch <- 1
ch <- 2
for v := range ch { // DEADLOCK: waiting for more values or close
fmt.Println(v)
}
// GOOD: Channel closed - loop exits
ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch) // Signal: no more values
for v := range ch { // Processes 1, 2, then exits
fmt.Println(v)
}
If the channel is never closed, the range loop will block forever waiting for more values, causing a deadlock.
Producer-Consumer Pattern
func producer(ch chan<- int) {
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(100 * time.Millisecond)
}
close(ch) // Important: signal completion
}
func consumer(ch <-chan int) {
for value := range ch { // Processes until channel closed
fmt.Println("Consumed:", value)
}
fmt.Println("Consumer done")
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch) // Blocks until producer closes channel
}
Range vs Manual Loop
Using Range (Recommended)
for value := range ch {
process(value)
}
Manual Loop (Equivalent)
for {
value, ok := <-ch
if !ok {
break
}
process(value)
}
Use range when possible - it’s more concise and idiomatic. Use manual loops when you need more control or must handle multiple channels.
Multiple Consumers
func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(time.Second)
}
fmt.Printf("Worker %d done\n", id)
}
func main() {
jobs := make(chan int, 10)
var wg sync.WaitGroup
// Start 3 workers
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, &wg)
}
// Send jobs
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs) // Signal: no more jobs
// Wait for all workers to finish
wg.Wait()
}
Pipeline Pattern
// Stage 1: Generate numbers
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Stage 2: Square numbers
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
// Stage 3: Sum numbers
func sum(in <-chan int) int {
total := 0
for n := range in {
total += n
}
return total
}
func main() {
// Set up pipeline
ch1 := generate(2, 3, 4)
ch2 := square(ch1)
result := sum(ch2)
fmt.Println("Sum of squares:", result) // 29
}
Each stage closes its output channel when done, allowing the next stage’s range loop to exit naturally.
Breaking Out Early
for value := range ch {
if value == target {
break // Exit early
}
process(value)
}
Breaking early leaves the channel unclosed and may leave the sender blocked. Ensure proper cleanup or use context cancellation.
Range with Select
You cannot directly use range with select, but you can combine them:
for {
select {
case value, ok := <-ch:
if !ok {
return // Channel closed
}
process(value)
case <-quit:
return // Shutdown signal
}
}
Practical Example: Log Processor
type LogEntry struct {
Timestamp time.Time
Level string
Message string
}
func logProducer(logs chan<- LogEntry) {
defer close(logs)
for i := 0; i < 10; i++ {
logs <- LogEntry{
Timestamp: time.Now(),
Level: "INFO",
Message: fmt.Sprintf("Log message %d", i),
}
time.Sleep(100 * time.Millisecond)
}
}
func logProcessor(logs <-chan LogEntry) {
for entry := range logs {
fmt.Printf("[%s] %s: %s\n",
entry.Timestamp.Format(time.RFC3339),
entry.Level,
entry.Message)
}
}
func main() {
logs := make(chan LogEntry, 5)
go logProducer(logs)
logProcessor(logs)
}
Fan-Out Pattern
func fanOut(in <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
ch := make(chan int)
channels[i] = ch
go func(out chan<- int) {
defer close(out)
// Each worker processes some items
for value := range in {
out <- value * 2
}
}(ch)
}
return channels
}
Fan-In Pattern
func fanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
// Start goroutine for each input channel
for _, ch := range channels {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for value := range c {
out <- value
}
}(ch)
}
// Close output when all inputs are done
go func() {
wg.Wait()
close(out)
}()
return out
}
Buffered vs Unbuffered
Unbuffered Channel
ch := make(chan int) // No buffer
go func() {
for i := 0; i < 5; i++ {
ch <- i // Blocks until received
}
close(ch)
}()
for v := range ch {
fmt.Println(v) // Each receive unblocks sender
}
Buffered Channel
ch := make(chan int, 5) // Buffer of 5
// Sender can send all values without blocking
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
// Receiver processes buffered values
for v := range ch {
fmt.Println(v)
}
Common Patterns
Counting Results
count := 0
for range ch {
count++
}
fmt.Println("Received", count, "items")
Collecting All Values
var results []int
for value := range ch {
results = append(results, value)
}
Processing Until Error
for item := range items {
if err := process(item); err != nil {
log.Printf("Error processing %v: %v", item, err)
break
}
}
range over channels has minimal overhead compared to manual loops. The compiler optimizes it efficiently.
Benchmarks (approximate)
- Range over channel: ~60-80 ns per iteration
- Manual loop: ~60-80 ns per iteration
- Overhead: Negligible (~1-2%)
Common Mistakes
Forgetting to Close
// BAD: Deadlock
ch := make(chan int)
go func() {
ch <- 1
ch <- 2
// Forgot close(ch)!
}()
for v := range ch { // Blocks forever after receiving 2
fmt.Println(v)
}
Closing Too Early
// BAD: Panic
ch := make(chan int)
close(ch)
ch <- 1 // PANIC: send on closed channel
Multiple Closers
// BAD: Race condition
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // Who closes?
}()
go func() {
for i := 5; i < 10; i++ {
ch <- i
}
close(ch) // PANIC: close of closed channel
}()
Best Practices
- Always close channels - When using range, ensure the channel is eventually closed
- Sender closes - Only the sender should close the channel
- Defer close - Use
defer close(ch) to ensure channels are closed
- Use range for simplicity - Prefer range over manual loops when possible
- One closer - Only one goroutine should close a channel
- Document ownership - Make it clear who is responsible for closing