---
title: 'select Statement'
source: 'https://academia.sh/en/courses/go-concurrency/select-statement'
course: Concurrency
language: en
updated: '2026-09-14T10:59:32+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Shows that select deterministically chooses the one branch that is ready, and that the branches' source order carries no priority"
    - 'Proves, by running it, that the default branch is chosen when no branch is ready, and how select is used as a non-blocking control tool'
    - 'Shows that selection is random when two branches are ready at the same time, and that this case is modeled, not run'
---

# select Statement

The select statement is the language's answer to waiting on more than one channel at the same time: when only one branch is ready, that branch is chosen deterministically; when none are ready and there is a default branch, that branch is chosen. Source order carries no priority. Bounding measurement: when two branches are ready at the same time, selection is random, and this case is not run, only modeled — randomness is the language's own decision.

The previous lesson measured where a channel's capacity starts blocking, and used `select`
with `default` only to poll a **single** channel. But a goroutine often waits for news from
more than one source at the same time: one of several result channels might be ready, a
cancellation signal might arrive, a timeout might run out. The `select` statement exists for
exactly this — the one structure that waits on more than one channel at the same time and
handles **whichever becomes ready first**.

This need shows up everywhere that waiting on a single channel is not enough. When a
goroutine is only waiting on one channel, writing `<-channel` is enough; but a goroutine that
wants to handle **whichever** of two separate sources arrives first cannot write this with a
single receive expression. `select` fills exactly this gap.

A `select` block contains more than one `case`, each starting with a channel operation. The
statement picks one of the ready operations and runs that branch; if none is ready and there
is a `default` branch, that runs immediately, otherwise `select` waits until at least one
branch is ready. This lesson measures how this choice gets made, where it is deterministic,
and where it is random.

Every branch of `select` can be a send or a receive; within the same `select`, some branches
can be sends and others receives, what matters is only which one can complete **at that
moment** without blocking. This is the first difference that sets `select` apart from a
`switch`: a `switch` compares a value against its branches in sequence, while `select` tries
every branch's own channel operation **at the same time** and decides based on which one is
ready. Writing a `case` that is not a channel operation does not compile; `select`'s branches
always start with a send or a receive.

The language sets no upper limit on how many branches a `select` can have; a `select` block
can wait on two channels or on ten. This lesson only uses two-channel examples, because the
rules being measured — deterministic selection, order independence, randomness — do not
depend on the number of branches; two branches are enough to show these rules and are the
easiest case to read. As the number of branches grows, the rules being measured do not
change, only the number of branches that can be ready at the same time grows; the third
section's rule about randomness also stays valid in exactly the same way when more than two
branches are ready.

## Selection Is Deterministic When Only One Branch Is Ready

If only one of the branches inside a `select` is ready, `select` runs that branch without
considering any other option. If none are ready and there is a `default` branch, `select`
falls to it without waiting at all — this is exactly the behavior used to poll capacity in
the previous lesson.

```go
// singleanddefault.go — selection is deterministic when only one branch is ready, default is chosen when none are
package main

import "fmt"

func choose(a, b chan string) string {
	select {
	case v := <-a:
		return "a: " + v
	case v := <-b:
		return "b: " + v
	default:
		return "default: neither is ready"
	}
}

func main() {
	a := make(chan string, 1)
	b := make(chan string, 1)

	fmt.Println("when neither is ready :", choose(a, b))

	a <- "first value"
	fmt.Println("when only a is ready  :", choose(a, b))

	b <- "second value"
	fmt.Println("when only b is ready  :", choose(a, b))
}
```

```
when neither is ready : default: neither is ready
when only a is ready  : a: first value
when only b is ready  : b: second value
```

**GC14.** — All three calls call the same `choose` function, with the same source code; the
only thing that changes is the fullness of the channels. In the first call, both are empty,
and `default` is chosen. Once a value is placed on `a`, the second call chooses the `a`
branch and, in doing so, **consumes** the value on `a` — `a` is now empty again. In the
third call, with only `b` full, the `b` branch is chosen. In no call were more than one
branch ready at the same time; this is why all three results are strictly repeatable.

It matters that this measurement can be done inside a single goroutine, requiring no
concurrency at all: values are placed on the `a` and `b` channels directly from the main
goroutine, no goroutine is started. This shows that `select` itself is not a construct
specific to concurrency — `select` is only a language feature that chooses among more than
one channel operation; whether or not there is a goroutine on the other side of the channels
does not change `select`'s own behavior. In later lessons `select` will always be used
between goroutines, but this lesson shows its core behavior in its plainest form, inside a
single goroutine.

This distinction looks small but matters: `select`'s deterministic behavior comes not from
timing, only from **which channels are full at that moment**. Had the same code filled the
channels in a different order, or run on a different machine, the result would still be the
same — because what decides it is not timing, but the fullness state itself.

## Source Order Is Not a Priority

`select`'s branches carry no top-to-bottom priority order — this is a fundamental
difference from an ordinary `if`/`else if` chain or a `switch`. Whichever branch is ready
gets chosen, no matter what order it was written in; source order exists only for
readability, it does not affect runtime behavior.

```go
// orderdoesnotmatter.go — the source order of select branches does not decide which branch is chosen
package main

import "fmt"

func choose(a, b chan string) string {
	select {
	case v := <-b:
		return "b: " + v
	case v := <-a:
		return "a: " + v
	}
}

func main() {
	a := make(chan string, 1)
	b := make(chan string, 1)
	a <- "only a filled"

	fmt.Println(choose(a, b))
}
```

```
a: only a filled
```

**GC15.** — In this function, the `b` branch is written **first** in the source, the `a`
branch **second**. When run with only `a` full, the result is still `a: only a filled` —
showing that the order in the source does not change the outcome. Every `case` in a `select`
is weighed equally; whichever branch is ready is the one that runs. This raises the next
section's question: what would happen if two branches were ready **at the same time**, given
that order carries no priority?

This behavior comes from how `select` evaluates its branches. The order written in the
source is only the order the compiler reads the `case`s in; at runtime, `select` evaluates
**all** of the branches at once, at the moment it runs, finds the ones that are ready, and
then chooses among those. In an ordinary `if a != nil { ... } else if b != nil { ... }`
chain, the first branch whose condition holds would win — there, source order really is a
priority. `select` does not follow that model; this difference is one of the points most
often confused when learning `select`, because its syntax looks a lot like a `switch`, but
its evaluation rule is different.

## Bounding Measurement: When Two Branches Are Ready at Once, Selection Is Random

Every measurement so far has set up situations where **at most one** branch is ready at any
given moment. What rule does the language apply when two branches are ready at the same
time? Go's own specification gives a clear answer to this question: when more than one
branch is ready at the same time, `select` chooses **uniformly at random** among them. This
is a **deliberate exception** to the determinism rule every measurement in this course has
rested on up to this lesson — and this is why this lesson does **not run** this case, only
models it.

```go
// modeledonly.go — when two branches are ready at once selection is random, this lesson does not run this, only shows it
package main

func chooseWhenBothReady(a, b chan string) string {
	select {
	case v := <-a:
		return "a: " + v
	case v := <-b:
		return "b: " + v
	}
}
```

**GC16.** — This function shows exactly the situation `select` faces when called with both
`a` and `b` full: the compiler makes no fixed preference between the two branches here, the
runtime chooses randomly again on every call. `chooseWhenBothReady` is never called anywhere
in this lesson — had it been called, it could have produced different results between two
runs, and this would deliberately break the "same output on every run" condition this
course's verification looks for. The function compiles only **because it exists**; the
source itself shows where the randomness shows up, not a run of it.

This randomness is not a shortcoming, it is a deliberate design decision. If `select` always
chose the first branch in the source between two ready branches, programmers would, over
time, start using this order like a priority rule, and this could lead to one of the channels
being systematically starved — if the same two branches stayed ready one after another, the
second branch might never run at all. Random selection removes this risk at the language's
own level: no branch can systematically get ahead just because it was written first in the
source.

This unmeasurability is the actual reason behind the previous section's finding that "source
order is not a priority." Had order carried a priority, which branch gets chosen when two
are ready at the same time would still be deterministic — the one written first would win.
Because order carries no weight at all, there is no real rule left to decide between two
branches that are both ready at once; the language fills this gap with randomness, a
different path from the "defined" class seen in the Go Fundamentals course: there, a
**fixed** value was placed in for something left unwritten, here, a value **drawn again
every time** is placed in for something left unwritten. This lesson does not carry this
distinction forward as an axis, it only notes it once.

Because this lesson never calls `chooseWhenBothReady`, the only way to show the difference
between two runs is to read the source. This is the one place, unlike every other lesson in
this course up through this one, where the claim "the same source can give a different result
on different runs" is proven **without running it**: Go's own specification guarantees that
the randomness exists, this lesson only shows, on the source itself, where that guarantee
takes effect.

## select Is a Non-Blocking Control Tool

The `default` branch turns `select` into more than a polling tool, into a control tool that
keeps the flow moving without ever stopping it. This can be used to drain every ready value
in a channel until none are left — the moment the channel empties, the loop stops itself
without waiting.

This use separates two different jobs `select` does. In the first section's use, `select`
was making a **choice** among more than one channel — deciding which source was ready. Here,
`select` is used on a single channel, to **ask** whether that channel is ready; the presence
of the `default` branch is the language's way of saying "do not wait if it is not ready."
Both uses share the same syntax, but their purposes differ: one decides among more than one
source, the other reads a single source's instantaneous state.

```go
// nonblockingcontrol.go — select is used to keep the flow moving without waiting on a channel that is not ready
package main

import "fmt"

func tryToReceive(channel chan int) (int, bool) {
	select {
	case v := <-channel:
		return v, true
	default:
		return 0, false
	}
}

func main() {
	results := make(chan int, 3)
	results <- 10
	results <- 20

	total, count := 0, 0
	for {
		v, ok := tryToReceive(results)
		if !ok {
			break
		}
		total += v
		count++
	}
	fmt.Println("values received without blocking from the channel:", count, "total:", total)

	_, ok := tryToReceive(results)
	fmt.Println("attempt once the channel is empty                :", ok)
}
```

```
values received without blocking from the channel: 2 total: 30
attempt once the channel is empty                : false
```

**GC17.** — Only two values were placed on the `results` channel; the loop drains these two
with `tryToReceive`, and because `ok` returns `false` on the third attempt, the loop stops on
its own — no goroutine was waited for, no order was left to the scheduler. This setup lets a
channel be asked "what do you have for me right now"; if the channel itself is empty, the
asking side can move on without waiting. This is the general form of the technique used to
poll buffer capacity in the previous lesson: there, whether a single channel was full was
being asked, here the same technique is used to **drain** a channel.

The loop's stopping condition also stands out: the `for` loop does not stop based on a fixed
number, it stops based on the second value `tryToReceive` returns. How many times it will
loop cannot be read from the source code — no matter how many values were placed on the
`results` channel, the same loop runs and stops at exactly that many steps. This differs from
walking a slice with `range`: how many times `range` will loop can be read **in advance**
from the slice's length, while how many times this loop will run is only learned by running
it. This difference becomes even clearer when compared against the `range over channel`
behavior the closing lesson measures: `range` counts a channel's **closing** as its stopping
condition, while the loop here counts the channel's current **fullness** as its stopping
condition — both produce a finite loop, but based on a different signal.

This way of using it has a cost: `tryToReceive` does not **wait** for a goroutine to produce,
it only takes whatever is ready at that moment. If nothing has been written to the channel
yet, `tryToReceive` always returns `false` — even if the producing goroutine is about to send
a value a microsecond later. A non-blocking control tool loses waiting in return; which one
is needed, when `select` should be allowed to wait and when it should move on immediately, is
a design decision.

This danger becomes even clearer with the wrong use of putting a `default` branch inside a
loop: if the goal is to wait for a value to arrive, and the loop retries immediately every
time it falls to `default`, the program waits without ever blocking, by keeping the processor
continuously busy — this is `select`'s counterpart of the **busy waiting** pattern set up in
the context of locks and critical sections in the Operating System Concepts course. This
lesson does not set up that pattern: `tryToReceive` is called once, not waited on in a loop.
When waiting is actually needed, the right tool is a `select` with no `default` — then
`select` itself blocks until a branch is ready and does not keep the processor busy; this
would be what the first section's `choose` function looks like written without a `default`
branch.

## Summary

- `select` deterministically chooses the one branch that is ready, among more than one
  channel operation, only when exactly **one** branch is ready.
- The branches' order in the source carries no priority; whichever branch is ready gets
  chosen no matter what order it was written in.
- When no branch is ready and there is a `default` branch, it runs immediately; `select`
  turns this way into a polling and control tool that never waits.
- When two branches are ready at the same time, selection is uniformly random; this is a
  deliberate exception to the determinism rule this course measures, and it is therefore not
  run, only modeled.
- `select`+`default` can be used to drain a channel of whatever values it holds, with no
  goroutine waited for, until none are left.
- A `select` with no `default` blocks when no branch is ready and does not keep the
  processor busy; this is the right tool when waiting is actually needed, not a loop with
  `default`.

## Next Step

This lesson never touched on how a closed channel behaves inside `select`. A receive from a
closed channel makes a `select` branch ready right away, exactly like a receive from a full
channel — but the answer that comes back is not the measured value itself, it is the news
that the channel **will now stay empty**. The next lesson measures this last behavior:
closing a channel is a termination signal, and it measures how that signal shows itself in
both `range` and a direct receive.
