Skip to main content

The select statement

7 minutes read•Filed underGo Programming Languageon

Learn how Go's select statement coordinates multiple channel operations simultaneously, handles timeouts, and powers the for-select pattern for reactive goroutines.

When you have a single channel, a simple send or receive is enough. But real concurrent programs rarely deal with just one channel — a goroutine might be waiting for data from multiple sources, or it might need to give up if nothing arrives within a deadline. Go's select statement exists precisely for this: it lets a goroutine wait on multiple channel operations at once and react to whichever one becomes ready first.

The select statement

At a glance, select looks like switch. Both use case clauses, both execute the matching branch, and both compile down to a decision tree. The syntax really is similar:

Each case must be a channel operation — either a send (ch <- value) or a receive (value := <-ch, <-ch). Unlike switch, you cannot put arbitrary boolean expressions in select cases.

Switch vs select

The resemblance to switch is mostly cosmetic. The two constructs behave very differently:

switchselect
Cases areEvaluated top-to-bottomEvaluated simultaneously
Branches onFirst true conditionFirst ready channel
BlocksNever (unless channel op in case)Until at least one case is ready
DefaultOptional short-circuitOptional non-blocking escape

The critical difference is simultaneous evaluation. In a switch, Go checks each case in order and stops at the first match. In a select, Go checks all cases at the same time and picks among the ones that are ready.

Both channels already have a value, so both cases are ready immediately. A switch would always pick ch1 because it's listed first. A select makes no such guarantee.

Simultaneous evaluation and pseudo-random selection

When multiple cases are ready at the same time, Go picks one using pseudo-random uniform selection — every ready case has an equal probability of being chosen. This is a deliberate design decision, not an implementation detail. If select always favored the first ready case, programs that relied on it would work correctly in tests but starve lower-priority channels under load. Randomness prevents systematic bias.

Running this a few times will produce different orderings. Neither channel dominates.

The default case

If no channel is ready and the select has a default clause, Go executes it immediately instead of blocking. This turns select into a non-blocking operation:

This is useful for polling — checking whether data is available without committing to wait for it. Be careful, though: a select with a default inside a tight loop becomes a busy-wait that burns CPU. Use it only when you genuinely want to proceed without data, not as a substitute for proper synchronization.

Timeouts with time.After

Blocking forever is usually wrong. A goroutine waiting on a channel that never sends will leak. time.After returns a channel that receives a value after the specified duration, making it a natural fit for select timeouts:

If ch delivers a message within two seconds, the first case runs. If two seconds pass with nothing, time.After's channel fires and the second case runs. The goroutine never blocks indefinitely.

The for-select pattern

A single select decides once and exits. Most goroutines need to keep reacting — reading from a stream, processing work items, or watching for a shutdown signal. The idiomatic way to do this is for-select: an ordinary for loop whose body is a select.

The goroutine spins in the loop, blocking at select each iteration until either a job arrives or the done channel signals shutdown. This pattern appears throughout the Go standard library and is the foundation of most long-lived concurrent goroutines.

A few things to notice in the example:

  • The ok idiom (j, ok := <-jobs) detects a closed channel. When a channel is closed and drained, ok is false. Checking it prevents the goroutine from looping forever on zero values.
  • The done channel uses struct{} — an empty struct has zero size and is the conventional type for signal-only channels that carry no data.
  • Both return statements are explicit. A break inside a select only breaks out of the select, not the enclosing for. To exit the loop from within a select, use return, or break with a labeled outer loop.

Putting it together

Here is a small self-contained example that combines for-select, a timeout, and a done signal to show how the pieces interact:

generate sends incrementing integers until done is closed. main reads them in a for-select loop, and after 100 milliseconds the timeout case fires, closes done, and exits. The generator goroutine detects the closed done channel in its own for-select and exits cleanly.

The select statement — especially inside a for loop — is the primary tool for building responsive, cancellable goroutines. Every goroutine you launch should have a way to exit; the done channel pattern shown here is one of the most common ways to provide that exit path. Without it, goroutines that block indefinitely become leaks — a silent, growing drain on memory that is easy to introduce and surprisingly hard to notice. That problem, and how to solve it systematically, is the subject of the next article.