select waits on many channels¶
Level: 101 · anyone whose goroutine has more than one channel to listen to
One line: A select blocks until one of its channel operations can proceed, then carries out exactly that one; give it a default case and it never blocks, which gives Go a send and a receive that give up instead of waiting.
Verified output of select_waits_on_many_go.go — regenerated by tools/run_examples.py, never hand-typed.
1. One select, two channels: it waits until either can proceed
7 selects: 3 from temperatures, 4 from humidities
2. default: a receive that does not wait
no job waiting, so default ran
received: resize image
3. default: a send that does not wait
queued: order-1
queued: order-2
dropped: order-3 (the buffer of 2 is full)
dropped: order-4 (nobody waiting on the unbuffered channel)
4. Every case is evaluated on entry, whichever case is chosen
label(order-5) ran
chose the receive: invoice-7
Reading the output¶
1. Waiting on two channels. Two goroutines send readings, one on each channel. main has a single select with a receive case for each. Each time round the loop, the select blocks until one of the two senders is ready, receives from it, and leaves the other channel alone. Seven selects took seven values: the three temperatures and the four humidities. Which channel delivered first changes from run to run, and the totals cannot change, so the program prints the totals. The Go specification ↗ describes exactly this: if no communication can proceed and there is no default case, the select blocks until at least one can.
2. A receive that does not wait. The same sentence of the spec has a middle step: if nothing can proceed and there is a default case, default runs. The first tryReceive found jobs empty and ran default at once. After a job was put in the buffer, the receive case could proceed, so it ran instead.
3. A send that does not wait. A send can proceed ↗ when a buffered channel has room, or when a receiver is ready on an unbuffered one. queue has room for two orders, so the third went to default and was dropped. That is the whole of a "queue it, or shed the load if the queue is full" check. handoff is unbuffered and no goroutine was receiving from it, so its send could not proceed and default ran. Without default, that select would wait for a receiver that never comes, and All goroutines are asleep shows what the runtime does then.
4. Every case is evaluated on entry. This select had a send case whose value comes from a function call, label("order-5"), and a receive case that was ready. The receive was chosen, and label still ran, before the choice was made. The spec says that on entering a select, the channel of every receive, and the channel and value of every send, are evaluated exactly once, in source order, and that their side effects happen whichever case is selected. So a case such as case out <- summarize(batch): calls summarize every time the select is entered, including every time the send is not made.
select_waits_on_many_go.go
select_waits_on_many_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// A select waits on several channel operations at once and carries out exactly
// one of them. A default case turns it into a send or receive that never waits.
//
// go run select_waits_on_many_go.go
package main
import "fmt"
func main() {
fmt.Println("1. One select, two channels: it waits until either can proceed")
temperatures := make(chan int)
humidities := make(chan int)
go func() {
for _, celsius := range []int{21, 22, 23} {
temperatures <- celsius
}
}()
go func() {
for _, percent := range []int{40, 45, 50, 55} {
humidities <- percent
}
}()
fromTemperatures, fromHumidities := 0, 0
for range 7 {
select { // blocks until one of the two senders is ready
case <-temperatures:
fromTemperatures++
case <-humidities:
fromHumidities++
}
}
fmt.Printf(" 7 selects: %d from temperatures, %d from humidities\n", fromTemperatures, fromHumidities)
fmt.Println()
fmt.Println("2. default: a receive that does not wait")
jobs := make(chan string, 1)
tryReceive := func() {
select {
case job := <-jobs:
fmt.Println(" received:", job)
default:
fmt.Println(" no job waiting, so default ran")
}
}
tryReceive()
jobs <- "resize image"
tryReceive()
fmt.Println()
fmt.Println("3. default: a send that does not wait")
queue := make(chan string, 2)
for _, order := range []string{"order-1", "order-2", "order-3"} {
select {
case queue <- order:
fmt.Println(" queued: ", order)
default:
fmt.Println(" dropped:", order, "(the buffer of 2 is full)")
}
}
handoff := make(chan string) // unbuffered, and no goroutine is receiving
select {
case handoff <- "order-4":
fmt.Println(" handed over: order-4")
default:
fmt.Println(" dropped: order-4 (nobody waiting on the unbuffered channel)")
}
fmt.Println()
fmt.Println("4. Every case is evaluated on entry, whichever case is chosen")
invoices := make(chan string, 1)
invoices <- "invoice-7"
select {
case handoff <- label("order-5"): // cannot proceed: nobody is receiving
fmt.Println(" sent order-5")
case invoice := <-invoices:
fmt.Println(" chose the receive:", invoice)
}
}
// label announces that it ran, so the output shows when select evaluated it.
func label(order string) string {
fmt.Println(" label(" + order + ") ran")
return order
}
What to do¶
- Reach for
selectwhen one goroutine has more than one thing to wait for: two inputs, an input and a timeout (A timeout is a channel), or an input and a cancellation (Cancel reaches every goroutine). - Add
defaultonly when not waiting is the point: to poll, or to drop work when a queue is full (A buffered channel is a bounded queue). Adefaultruns at once, so aforloop around aselectwhosedefaultdoes nothing comes straight back round. That loop spins without ever waiting. - Keep work out of case expressions. Compute a value before the
select, or accept that it is computed on every pass, sent or not. - Print what arrived, not the order it arrived in. A
selectover several producers interleaves them differently on every run.
In other languages¶
- The Rust library's Channels ↗ page uses
std::sync::mpsc, which has noselect. ItsReceiver↗ cantry_recv, which is Go's receive with adefault, but every method waits on that one receiver. To wait on several at once, Rust code uses a crate:crossbeam_channel::select!↗, which also takes adefaultcase, ortokio::select!↗ in async code. - The Concurrency library's chapter 05, Message passing, which covers channels and
selectacross languages, is planned.
Sources¶
- The Go specification: Select statements ↗ and Send statements ↗.
- A Tour of Go, Default Selection ↗.
- Katherine Cox-Buday, Concurrency in Go (O'Reilly, 2017), chapter 3, "Go's Concurrency Building Blocks", the section on the
selectstatement. - James Cutajar, Learn Concurrent Programming with Go (Manning, 2024), chapter 8, "Selecting channels".
- Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 12, "Concurrency in Go", the section on
select.