Range over Channels
The range
keyword in Go provides a convenient way to iterate over values received from a channel. It automatically handles waiting for new values and will exit when the channel is closed.
Basic Range over Channel#
Here's the basic syntax for using range with channels:
for value := range ch {
string">"comment">// Process value
}
When using range
on a channel:
- It iterates over each value received from the channel
- The loop automatically breaks when the channel is closed
- Unlike a normal receive operation (
value, ok := <-ch
), range doesn't return the second boolean value that indicates whether the channel is closed
Simple Example#
Let's look at a simple example of using range with channels:
package main
import string">"fmt"
func main() {
ch := make(chan int)
string">"comment">// Start a goroutine that sends values
go func() {
for i := 0; i < 6; i++ {
string">"comment">// Send iterator to channel
ch <- i
}
string">"comment">// Close the channel when done
close(ch)
}()
string">"comment">// Range over the channel to receive values
for v := range ch {
fmt.Println(v)
}
}
When you run this program, you'll see:
0
1
2
3
4
5
In this example:
- We create a channel
ch
- We start a goroutine that sends values 0 through 5 to the channel and then closes it
- We use
range
to receive values from the channel - The
range
loop automatically exits when the channel is closed
Range vs. Manual Receive#
Let's compare using range
with manual receive operations:
string">"comment">// Using range (cleaner, more idiomatic)
for v := range ch {
fmt.Println(v)
}
string">"comment">// Using manual receive (more verbose)
for {
v, ok := <-ch
if !ok {
string">"comment">// Channel is closed
break
}
fmt.Println(v)
}
As you can see, using range
is more concise and less error-prone.
Handling Channel Closure#
When using range
, you don't need to explicitly check if the channel is closed. The loop will automatically exit when the channel is closed.
This makes range
particularly useful in consumer code, where you often want to process all values until the producer indicates it's done by closing the channel.
Example: Worker Pool with Range#
Here's an example of a worker pool that uses range
to process jobs:
package main
import (
string">"fmt"
string">"sync"
string">"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
string">"comment">// Use range to process jobs until the channel is closed
for job := range jobs {
fmt.Printf(string">"Worker %d processing job %d\n", id, job)
time.Sleep(time.Second) string">"comment">// Simulate work
results <- job * 2
}
fmt.Printf(string">"Worker %d done\n", id)
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
string">"comment">// Create a wait group to wait for all workers to finish
var wg sync.WaitGroup
string">"comment">// Start 3 workers
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
string">"comment">// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
string">"comment">// Close the jobs channel to signal no more jobs
close(jobs)
string">"comment">// Start a goroutine to close the results channel when all workers are done
go func() {
wg.Wait()
close(results)
}()
string">"comment">// Use range to collect all results
for result := range results {
fmt.Printf(string">"Result: %d\n", result)
}
}
In this example:
- We use
range
in the worker function to process jobs until the jobs channel is closed - We close the jobs channel to signal to the workers that there are no more jobs
- We use
range
in the main function to collect all results until the results channel is closed - We close the results channel when all workers are done
Using Range with Select#
When using range
with select
, you need a different approach since range
is a loop and select
is a block. Here's a pattern that combines both:
func processMessages(messages <-chan string, quit <-chan bool) {
for {
select {
case msg, ok := <-messages:
if !ok {
string">"comment">// messages channel is closed
return
}
fmt.Println(string">"Received:", msg)
case <-quit:
fmt.Println(string">"Quitting")
return
}
}
}
Best Practices#
1. Always Close Channels from the Sender#
When using range
, it's important to remember that the sender should close the channel when there are no more values to send. The closer of a channel should be the only goroutine that knows when the channel should be closed.
string">"comment">// Good: Sender closes the channel
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
string">"comment">// Bad: Receiver closes the channel
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
}()
string">"comment">// ...
close(ch) string">"comment">// Dangerous! The sender might still be trying to send
2. Use Done Channels for Signaling Completion#
When you need bidirectional communication or complex coordination, consider using done channels:
func worker(jobs <-chan Job, done chan<- bool) {
for job := range jobs {
process(job)
}
done <- true
}
3. Use WaitGroups for Multiple Workers#
When you have multiple workers, use a WaitGroup to wait for all of them to finish:
func main() {
jobs := make(chan Job)
var wg sync.WaitGroup
string">"comment">// Start workers
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
process(job)
}
}()
}
string">"comment">// Send jobs
string">"comment">// ...
string">"comment">// Close the jobs channel
close(jobs)
string">"comment">// Wait for all workers to finish
wg.Wait()
}
Summary#
- The
range
keyword provides an easy way to iterate over values from a channel - It automatically handles waiting for values and detecting channel closure
- It's more concise and less error-prone than manual receives
- Always close channels from the sender, not the receiver
- Combine
range
with WaitGroups for coordinating multiple workers
In the next lab, we'll take a deeper look at unbuffered channels and their synchronization properties.