select
select waits on several channel operations at once and runs whichever one becomes ready first.
select { case v := <-in: ...; case out <- v: ...; case <-time.After(d): ...; default: ... }A plain receive handles one channel. select handles several. It lists channel operations as cases, waits until one of them can proceed, and runs that one.
Every case is a send or a receive, not a condition. case msg := <-a: runs when a value can be received from a. case b <- v: runs when a value can be sent on b. If more than one case is ready, Go picks one at random, so you cannot rely on any order between cases.
default and time.After
A default case changes the waiting entirely. With one present, select never blocks: if no channel operation can proceed at that instant, default runs instead. That is how you write a send that drops its value rather than waiting for room.
For a deadline, add a case on a channel that becomes ready after a delay. time.After(d) returns a <-chan Time which receives one value once d has passed. Whichever case is ready first wins, so a slow channel loses to the timer.
What trips people up
A select with a default inside a loop spins a CPU core flat out, because it never waits for anything. Either drop the default or give the loop something real to block on.
A closed channel is always ready to receive, so a case on one is chosen over and over. Set that variable to nil once the channel is drained: a nil channel is never ready, which takes the case out of the running.
Since Go 1.23 the timer behind time.After can be collected before it fires, so using it in a loop no longer holds memory until the delay elapses.
Example
package main
import (
"fmt"
"time"
)
func main() {
fast := make(chan string, 1)
slow := make(chan string)
fast <- "fast result" // buffered, so this send does not block
// select waits until one of its cases can run, then runs that one.
select {
case msg := <-fast:
fmt.Println("got:", msg)
case msg := <-slow:
fmt.Println("got:", msg)
case <-time.After(2 * time.Second):
// time.After returns a channel that gets a value after the wait.
fmt.Println("timed out")
}
// A default case makes select non-blocking. If nothing is ready at
// this instant, default runs instead of waiting.
select {
case msg := <-slow:
fmt.Println("got:", msg)
default:
fmt.Println("nothing ready")
}
}
// Output:
// got: fast result
// nothing readyWhen to use it
- Give a slow upstream call a two-second deadline and return an error instead of hanging.
- Drop a metrics sample when its buffer is full rather than slowing the request.
- Read from a results channel and a shutdown channel in the same loop.
- Merge two event streams into one handler goroutine.
More examples
Reading two sources in one loop
The basic shape: a loop with a <code>select</code> in it, pulling from whichever source has something ready. The counts are fixed but the interleaving is not, which is the single most important thing to internalise about <code>select</code>. Any test that asserts the order values arrive in from two independent senders will pass locally and fail on a busy machine. Use this when one goroutine must service several streams, for example results from workers and a shutdown signal.
package main
import "fmt"
func main() {
letters := make(chan string)
numbers := make(chan string)
go func() {
letters <- "a"
letters <- "b"
}()
go func() {
numbers <- "1"
numbers <- "2"
}()
// Four values are coming, two from each sender. select takes whichever
// is ready, so the order the two sources interleave in is not fixed.
// The counts are, which is why this prints the same thing every run.
fromLetters, fromNumbers := 0, 0
for i := 0; i < 4; i++ {
select {
case <-letters:
fromLetters++
case <-numbers:
fromNumbers++
}
}
fmt.Println("letters:", fromLetters, "numbers:", fromNumbers)
}
// Output:
// letters: 2 numbers: 2Dropping on a full buffer, and giving up after a delay
Two ways to refuse to wait, side by side. <code>default</code> gives up immediately, which is right for data you would rather lose than let slow down the request that produced it: metrics samples, debug log lines, cache warm-ups. <code>time.After</code> gives up after a delay, which is right when the value matters enough to wait a little. Note that both cases here are sends, not receives. A <code>select</code> case can be either, and a send case with a <code>default</code> is the standard non-blocking send.
package main
import (
"fmt"
"time"
)
func main() {
// A log sink nobody is draining. Capacity 1, so it fills at once.
logs := make(chan string, 1)
for i := 0; i < 3; i++ {
msg := fmt.Sprintf("event %d", i)
select {
case logs <- msg:
fmt.Println("queued", msg)
default:
// The buffer is full. Dropping a log line beats blocking the
// request that produced it.
fmt.Println("dropped", msg)
}
}
// A deadline instead of an immediate drop. Nothing receives, so the
// timer case wins after 50 milliseconds.
select {
case logs <- "final":
fmt.Println("queued final")
case <-time.After(50 * time.Millisecond):
fmt.Println("gave up on final")
}
}
// Output:
// queued event 0
// dropped event 1
// dropped event 2
// gave up on finalDisabling a case by setting the channel to nil
The fix for the most common <code>select</code> bug. Merging two streams and stopping when both end looks simple, but a closed channel is permanently ready, so the loop would receive zero values from it as fast as the CPU allows. Assigning <code>nil</code> to the variable removes that case, because a receive from a nil channel can never proceed. The loop condition then ends the loop once both are nil. Closing and the two-result receive used here are covered in Closing Channels and range.
package main
import "fmt"
func main() {
a := make(chan string)
b := make(chan string)
go func() {
a <- "from a"
close(a)
}()
go func() {
b <- "from b"
close(b)
}()
// A closed channel is always ready to receive, so leaving it in the
// select would spin. Receiving from a nil channel never proceeds, so
// setting a drained channel to nil takes its case out of the running.
received := 0
for a != nil || b != nil {
select {
case _, ok := <-a:
if ok {
received++
} else {
a = nil
}
case _, ok := <-b:
if ok {
received++
} else {
b = nil
}
}
}
fmt.Println("values received:", received)
}
// Output:
// values received: 2
Discussion