Waiting on several channels
select looks like a switch, but each case is a channel send or receive. It blocks until one case can proceed and then runs that one.
With these delays the first select gets the fast result and the second gets the slow one. Neither receive has to wait for the other channel. A plain <-slow followed by <-fast would handle them in a fixed order no matter which arrived first.
How select evaluates:
- All channel expressions and the values to send are evaluated once, in source order, when the
selectstarts. - If one or more cases are ready, one of them is chosen at random.
- If none is ready and there is a
default, thedefaultruns. - Otherwise the goroutine blocks until some case becomes ready.
An empty select {} blocks forever. You occasionally see it at the end of main in programs whose real work happens in other goroutines.
Random choice among ready cases
When several cases are ready at the same time, select does not prefer the first one listed. This program fills two buffered channels and then selects 1000 times:
The split between countA and countB changes every run and lands near 500 each. The random choice is deliberate: it prevents a busy channel from starving the others. If you need priority, see the pattern further down.
Non-blocking operations with default
With a default case, select never blocks. That turns a send or a receive into a "try" operation:
Dropping work when a buffer is full is how you shed load or emit metrics without ever stalling the caller.
Do not put a default in a select inside a for loop just to "check" channels over and over. With nothing ready, the loop spins at 100% CPU. Block instead, and add a timeout case if you need to wake up periodically.
Timeouts
time.After(d) returns a channel that receives once after d. Race it against the real work:
The first call returns "data". The second returns the timeout error after 50 ms, long before the worker would have finished. The result channel has a buffer of 1 on purpose. When the timeout wins, nobody ever receives from result; with an unbuffered channel the worker goroutine would block on its send forever and leak.
In a loop, time.After creates a new timer on every iteration, which is exactly right for a per-message idle timeout ("no message for 1 second"). For an overall deadline across many operations, create one timer or context before the loop. Since Go 1.23, timers that are no longer referenced are garbage collected even if they have not fired, so time.After in a loop no longer holds memory until each timer fires, as it did in older versions (this needs go 1.23 or later in go.mod).
for-select loops and quit channels
A goroutine that runs until told to stop is a for loop around a select with one case for work and one for stopping:
Closing quit rather than sending on it is the idiom: a close is seen by every receiver, now and later, so one close stops any number of workers. The done channel lets main wait until the worker has really returned.
In real code the quit channel is usually a context.Context: case <-ctx.Done():. It works the same way (Done() returns a channel that is closed on cancellation) and also carries deadlines and the reason for stopping. The context page covers it.
break inside select
break in a select case exits the select, not the enclosing for. This is a common source of loops that never end. Use return, or label the loop:
Priority between channels
Since select picks randomly, you cannot rank cases inside one statement. To make one channel win whenever it has something, check it first on its own:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
The first select returns immediately if cancellation already happened. Without it, a steady stream of jobs could keep winning the random choice for a while after ctx was cancelled.
Nil channels disable a case
A send or receive on a nil channel is never ready, so a case on a nil channel is effectively switched off. Setting a closed input to nil is how you stop selecting on it while continuing with the others; the channels page shows a merge loop built this way. The same trick switches a timeout on and off: keep var timeout <-chan time.Time as nil until you need it, then assign time.After(d).
Common mistakes
- Expecting source order. The first listed case is not preferred.
- A busy loop with
default. Afor { select { ... default: } }with nothing to do burns a CPU core. - Leaking the loser. When a timeout wins, the goroutine that would have sent the result must still be able to finish. Give its channel a buffer of 1.
breakthat only leaves theselect. Use a label orreturn.- One
time.Afterper loop iteration used as an overall deadline. It restarts every iteration; create the deadline once, outside the loop.
Frequently Asked Questions
What does select do in Go?
select waits until one of its channel operations (a send or a receive) can proceed, then runs that case. If several are ready at the same time it picks one at random. Without a default case it blocks until some case is ready; with default it never blocks.
How do I add a timeout to a channel receive in Go?
Put the receive and a timer in one select: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. Whichever happens first wins. Inside a loop, or when a caller already has a deadline, use a context.Context with context.WithTimeout and select on ctx.Done() instead.
Does select in Go pick cases in order?
No. When more than one case is ready, Go chooses uniformly at random, so no case can starve the others. If you need priority, check the high-priority channel in its own select with a default first, then fall through to a select over all channels.
Why does break not exit my for-select loop?
Inside a select, break exits only the select statement, not the surrounding for. Use return, or put a label on the loop (loop: for { select { case <-done: break loop } }).