select
Picture a waiter standing between two serving hatches: hot dishes come through one, drinks through the other. If the waiter waits only at the hatch for hot dishes, ready drinks sit there until a hot dish is served. It works better to stand between the hatches and take whatever appears first.
Channels work the same way. A receive from a channel waits until a value appears in the channel. When a value may arrive on either of two channels, waiting on one of them does not work: <-nums waits forever if the value came into words.
Waiting on several channels
The select statement waits on several channel operations at once and runs the one that is ready:
package main
import "fmt"
func main() {
nums := make(chan int)
words := make(chan string, 1)
words <- "go"
select {
case n := <-nums:
fmt.Println("number", n)
case w := <-words:
fmt.Println("word", w) // word go
}
}Let's walk through the example step by step:
nums := make(chan int)creates a channel nobody sends into.words := make(chan string, 1)creates a buffered channel, andwords <- "go"puts"go"in the buffer.selectwatches both hatches: the receive fromnumsis not ready, the receive fromwordsis.- The second case runs, and the program prints
word go.
The cases of select
select is written like switch, but each case in it is a channel operation. There are two such operations.
Receive
The case case w := <-words: runs when a value can be received from the channel. A variable declared in a receive, like w in the example, exists only inside its own case. The received value need not be stored: case <-done:.
Send
A case can also send: case ch <- v: runs when the channel is ready to take the value.
How a case is chosen
select follows three rules:
- if no operation is ready,
selectwaits until one becomes ready; - only one operation runs: the other cases are skipped, and no value is received from their channels;
- if several operations are ready,
selectpicks one of them at random.
This is where the waiter picture stops working: a person would walk to the nearer hatch, while select draws lots.
The order of the cases in the code does not affect the choice, unlike switch, which checks from top to bottom. So a program with several ready channels may print different things on different runs. Interviewers like to ask this: which case runs if both are ready? The answer is either one; the choice is random.
The default case
The default branch runs when no operation is ready, and then select does not wait:
ch := make(chan int, 1)
select {
case v := <-ch:
fmt.Println(v)
default:
fmt.Println("empty") // empty
}Here is what happens:
- the channel is empty, so
defaultruns: the waiter glanced at the hatch, saw an empty shelf and went off to other work; - if a value is sent into
chbefore theselect, the first case runs and prints it.
This is how a goroutine checks a channel without stopping: a receive from an empty channel without select would wait until someone sends a value.
Common mistake
A select sits inside a for loop, and the loop is meant to end with break when the done channel is closed:
for {
select {
case v := <-nums:
fmt.Println(v)
case <-done:
break // ✗
}
}Here is what happens:
- A
breakinsideselectleaves only theselect, as it does inswitch. - The
forloop goes on. - A closed
donechannel is always ready to receive, so the loop spins forever.
There are two ways to fix it:
- move the loop into its own function and replace
breakwithreturn; - put a label before
forand writebreakwith its name.
Key points
selectwaits on several channel operations and runs one that is ready.- If several are ready, the choice is random; the order of the cases in the code does not matter.
defaultruns when nothing is ready, and thenselectdoes not wait.- A
breakinsideselectleaves only theselect, not the loop around it.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.