Skip to main content
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:
one
two
The range loop continues receiving from the channel until the channel is closed and all buffered values are consumed.

How Range Works with Channels

  1. Receives a value from the channel
  2. Assigns the value to the loop variable
  3. Executes the loop body
  4. Repeats until the channel is closed and empty
  5. 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

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
	}
}

Performance Considerations

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

  1. Always close channels - When using range, ensure the channel is eventually closed
  2. Sender closes - Only the sender should close the channel
  3. Defer close - Use defer close(ch) to ensure channels are closed
  4. Use range for simplicity - Prefer range over manual loops when possible
  5. One closer - Only one goroutine should close a channel
  6. Document ownership - Make it clear who is responsible for closing

Build docs developers (and LLMs) love