select
Prerequisites: Channels
select waits on several channel operations at once and runs the first one
that is ready. It looks like a switch, but each case is a send or a
receive.
- If several cases are ready at the same time, one is chosen at random.
- A
defaultcase runs immediately when no other case is ready, making theselectnon-blocking. time.After(d)returns a channel that receives a value afterd, which makes timeouts a one-liner.
Syntax
select {
case msg := <-messages:
fmt.Println("received", msg)
case results <- value:
fmt.Println("sent", value)
case <-time.After(time.Second):
fmt.Println("timed out")
default:
fmt.Println("nothing ready")
}
Example
package main
import (
"fmt"
"time"
)
// fetch simulates a slow operation that delivers its result on a channel.
func fetch(delay time.Duration, result string) <-chan string {
ch := make(chan string, 1) // buffered so the goroutine never blocks forever
go func() {
time.Sleep(delay)
ch <- result
}()
return ch
}
func main() {
// Take whichever result arrives first.
fast := fetch(10*time.Millisecond, "fast")
slow := fetch(300*time.Millisecond, "slow")
select {
case r := <-fast:
fmt.Println("got", r)
case r := <-slow:
fmt.Println("got", r)
}
// Give up after a timeout.
select {
case r := <-fetch(500*time.Millisecond, "too late"):
fmt.Println("got", r)
case <-time.After(50 * time.Millisecond):
fmt.Println("timed out")
}
// Non-blocking receive with default.
msgs := make(chan string, 1)
select {
case m := <-msgs:
fmt.Println("received", m)
default:
fmt.Println("no message ready")
}
msgs <- "hi"
select {
case m := <-msgs:
fmt.Println("received", m)
default:
fmt.Println("no message ready")
}
}
Expected output
got fast
timed out
no message ready
received hi
The first two results depend on timing, but the delays are far enough apart that you will see this output in practice.
A common pattern is a for-select loop that processes work until told to
stop:
for {
select {
case job := <-jobs:
process(job)
case <-done:
return
}
}
Common mistakes
- Using
defaultinside aforloop with nothing else to wait on — the loop spins and burns 100% of a CPU core. Only adddefaultwhen you really want a non-blocking check. - Creating a new
time.Afteron every iteration of a busy loop when you meant one overall deadline. Create the timer channel once, outside the loop, or usetime.NewTimer/time.NewTicker. - Expecting cases to be tried top to bottom. When several are ready, the choice is random.
- Returning an unbuffered channel from a function like
fetch. If the caller times out and never receives, the goroutine blocks on its send forever — a goroutine leak.
Practice
Write firstOrTimeout(ch <-chan int, d time.Duration) (int, bool) that
returns the first value from ch, or 0, false if nothing arrives within
d. Test it with a buffered channel that already holds 42, and with an
empty channel.
Show solution
package main
import (
"fmt"
"time"
)
func firstOrTimeout(ch <-chan int, d time.Duration) (int, bool) {
select {
case v := <-ch:
return v, true
case <-time.After(d):
return 0, false
}
}
func main() {
ready := make(chan int, 1)
ready <- 42
fmt.Println(firstOrTimeout(ready, 100*time.Millisecond)) // 42 true
empty := make(chan int)
fmt.Println(firstOrTimeout(empty, 50*time.Millisecond)) // 0 false
}