Select

select is a powerful tool for managing multiple channels in Go, letting you wait on I/O operations and avoid unnecessary blocking. Understanding its syntax, the importance of default and the behavior when multiple cases are ready is essential to writing robust concurrent code.
What is select?
selectlets a goroutine wait on multiple channel operations at the same time.- It works much like a
switch, but eachcaseinvolves a send or receive operation on a channel.
Syntax
- The basic structure of select is:
select { case v := <-ch1: // code to run when a value comes from ch1 case ch2 <- 10: // code to run when it is possible to send the value 10 to ch2 default: // optional code run if none of the operations above is ready } - Each
casetries to perform a channel operation. - If more than one
caseis ready,selectpicks one of them at random. - If no
caseis ready and there is adefault, thedefaultruns immediately. - If there is no
default, the goroutine blocks until one of the operations can be performed.
When to use select
- To manage timeouts on channel operations.
- To synchronize several channels and handle the first available operation.
- To avoid blocking when no operation is ready, using default.
Practical example
Consider two channels; we want to wait for data from either of them:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// Simulating data being sent after different delays
go func() {
time.Sleep(500 * time.Millisecond)
ch1 <- "Message from channel 1"
}()
go func() {
time.Sleep(300 * time.Millisecond)
ch2 <- "Message from channel 2"
}()
// select waits until one of the channels is ready to send data
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
default:
fmt.Println("No channel was ready.")
}
// The program can wait a bit to see the full execution
time.Sleep(1 * time.Second)
}
In this example, because of the sleep times, the ch2 case will fire first, but the default would run immediately if no channel were ready.
Using default
defaultruns when no other channel operation is ready.- It can be used to keep select from blocking when no operation is available.
Exercises with select
Exercise 1 – Receiving from the first available channel
- Requirement: Create two string channels and launch two goroutines that send messages to them after different delays.
- Use select to print the message that arrives first.
Exercise 2 – Send and default
- Requirement: Create an int channel with no receiving routine.
- Use select with one case to send a value and a default that prints “No send possible right now”.
- See how default avoids the blocking.
Exercise 3 – Timeout with select
- Requirement: Create an int channel and launch a goroutine that doesn’t send any data.
- In main, use select with one case to receive from the channel and a default or time.Sleep that simulates a timeout, printing “Timeout reached”.
Exercise 4 – Alternating between multiple channels
- Requirement: Create three string channels and launch goroutines that send messages to each one, with different sleeps.
- Use select in a loop to receive the messages as soon as they are available and print the message and the channel identifier.
Exercise 5 – Combination: select with waitgroup
- Requirement: Combine sync.WaitGroup and select.
- Launch two goroutines, each sending a value to a channel after a different delay.
- In main, use select to capture the first value received and then wait on the WaitGroup to synchronize the end of both goroutines.
- Print both values, but highlight which one arrived first via select.
Most common mistakes with select
Failing to include a default when needed
- Mistake: If none of the channel operations is immediately possible and there is no default, select blocks the goroutine indefinitely.
- Solution: Use default for non-blocking operations, when appropriate.
Assuming a deterministic order
- Mistake: Expecting the select cases to run in a fixed order.
- Reality: If more than one case is ready, select picks one of them at random.
Not handling closed channels
- Mistake: Trying to receive from a channel that has already been closed without checking the “ok” value can lead to wrong interpretations (the zero value of the type is returned).
- Solution: Always check “value, ok := <-ch” when it is relevant to the context.
Using select badly in loops
- Mistake: Putting select inside loops without control can lead to busy waiting (excessive CPU use) if a default or a pause is not implemented.
- Solution: Control the loops with proper conditions or use time.Sleep to avoid needless CPU use.
Mixing blocking and non-blocking cases
- Mistake: Having cases that send/receive data in a blocking way together with a default case can lead to unexpected behavior, where the default fires immediately without trying the blocking operations.
- Note: Understand the evaluation order and the blocking behavior of channels.
Exercise solutions
So I wouldn’t have to run each exercise by hand, I used tests and testable examples.
Exercise 1:
// main.go
// main_test.go
Next steps!
Next we will cover Mutex! Click here for part 4!