skip to content

select and Multiplexing

select waits on several channel operations and proceeds with whichever is ready, choosing at random when more than one is — so case order gives you no priority. The for-select loop with a cancellation case is the shape most idiomatic Go concurrency answers are built from.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

In Go, what does a `select` statement do that a plain channel receive like `v := <-ch` cannot?

level: juniorimportance: must knowfreq 78%

answer

  1. one goroutine, several channels
  2. it waits for the first thing possible
  3. cases are operations, not values
  4. only one case body runs per pass
  5. ties are broken at random, not by order

basics

~20 s

A select waits on several channel operations at once and proceeds with whichever one is ready first. A plain receive waits on exactly one channel, so a goroutine sits there even when a different channel has work for it.

solid answer

~40 s

A `select` lists several channel operations as cases; each case is a receive or a send. Control blocks in the select until at least one of those operations can proceed, then that one operation is performed and only that case's body runs. If several cases are ready at the same instant, Go picks one uniformly at random rather than by source order. That is what lets a single goroutine multiplex sources it cannot predict the order of, such as keystrokes, resize events and redraw requests arriving on three separate channels, plus a `ctx.Done()` case to stop. Without `select` you would need one goroutine per channel and some other way to join them. A `select` can also carry a `default` case, which turns it into a non-blocking attempt.

code

go · 15 lines
go
// a terminal dashboard serving three input sources plus cancellation
func run(ctx context.Context, keys <-chan rune, resizes <-chan [2]int, redraw <-chan struct{}) {
	for {
		select {
		case k := <-keys:
			handleKey(k)
		case size := <-resizes:
			resizeTo(size)
		case <-redraw:
			paint()
		case <-ctx.Done():
			return
		}
	}
}

go deeper

for a junior

Be ready to write the shape from memory: a for loop, one case per input channel, and a case that returns. Say plainly that it blocks until one case can run and that exactly one body executes.

for a middle

An interviewer expects the mechanics: which operations count as ready, that a closed channel makes its receive case permanently ready, and that ties are broken uniformly at random rather than by source order.

for a senior

Show that you treat the loop as a single-threaded event dispatcher: work done inside a case body blocks every other source, so slow work is handed to another goroutine and the select goes straight back to waiting.

for a principal

Frame it as an API question. A component that owns a for-select loop should expose one channel per concern and one cancellation path, because callers cannot see or reorder the cases inside your loop.

## What a select statement is A `select` looks like a `switch`, but its cases are not values to compare — each case is a **channel operation**: a receive (`case v := <-keys:`), a receive with the two-value form (`case v, ok := <-keys:`), a bare receive (`case <-redraw:`), or a **send** (`case out <- v:`). ```go select { case k := <-keys: handleKey(k) case <-redraw: paint() } ``` ## How it executes 1. Every case's channel expression (and, for a send case, the value being sent) is evaluated once, in source order, on entering the statement. 2. The runtime looks at which of those operations **can proceed right now** — a receive whose channel has a value queued or is closed, a send whose channel has room or has a receiver waiting. 3. If exactly one can proceed, that operation is performed and that case's body runs. 4. If **several** can proceed, one is chosen **uniformly at random**. Source order does not make an earlier case win. 5. If **none** can proceed and there is no `default` case, the goroutine blocks inside the select until one of them becomes possible. 6. Exactly **one** case body runs per execution of the statement. The others do not run, and the operations they name do not happen. That last point is what beginners most often get wrong: a select is not "do all of these", and it is not "try them in order". It is "wait for the first thing that becomes possible, do that one thing". ## Why a plain receive is not enough `v := <-keys` commits the goroutine to one channel. If the user resizes the window instead of typing, the goroutine is still parked on `keys` and the resize sits unhandled. You could start a goroutine per channel, but then every one of them touches the same screen state and you have introduced a data race you now need a mutex to fix. A single goroutine with a `select` keeps all the state in one place and serialises the events for free — one event handled at a time, in whatever order they arrive. ## The for-select loop A select handles one event. To keep serving, wrap it in a loop: ```go for { select { case k := <-keys: handleKey(k) case <-ctx.Done(): return } } ``` This shape — a loop, one case per input source, and one case that means *stop* — is the standard skeleton of every long-lived goroutine in Go. The stop case is usually `<-ctx.Done()` on a `context.Context` passed in by whoever started the goroutine, or a receive from a `done chan struct{}`. Because a cancelled context's Done channel is **closed**, a receive on it succeeds immediately and forever, so that case becomes permanently ready and the loop can return. Every case body must either fall out of the switch and go round the loop again, or `return`. A case body that blocks for a long time stops the whole loop: the goroutine is inside that body, not inside the select, so nothing else is being served meanwhile. ## Blocking, and the empty select With no ready case and no `default`, a select blocks. Taken to its limit, `select {}` — a select with no cases at all — can never have a ready case, so it blocks its goroutine forever. It is occasionally written at the end of `main` to park the main goroutine while background goroutines do the work. If it is the only goroutine left with nothing to wake it, the runtime detects that every goroutine is asleep and aborts the program. ## Random choice, briefly The uniform-random tiebreak is deliberate. If select preferred the first listed case, a channel that is almost always ready would starve every case below it. Randomising means each ready case is equally likely, so a busy source cannot monopolise the loop. The practical consequence — that a `ctx.Done()` case does **not** win just because it is ready — is worth internalising early.

  • What does an empty `select {}` with no cases do?
    It has no case that could ever become ready, so it blocks its goroutine forever. People write it at the end of `main` to park the main goroutine while background goroutines keep serving. If nothing else can run, the runtime notices every goroutine is asleep and aborts the program instead of hanging silently.
  • Can a case in a select be a send rather than a receive?
    Yes. `case out <- v:` is eligible only when that send can proceed — the channel has buffer room or a receiver is already waiting. Mixing a send case with receive cases lets one goroutine wait to hand work off or accept new work, whichever becomes possible first.
  • How many of the case bodies run when three of the cases are ready?
    Exactly one. The runtime performs one of the three channel operations, chosen uniformly at random, and runs only that case's body. The other two operations do not happen at all — those values stay in their channels for a later pass of the loop.

saying these in an interview costs you the question

  • Says select tries the cases top to bottom in order
  • Thinks every ready case's body runs
  • Thinks select returns immediately when no channel is ready
  • Describes select as a switch on a channel's value
  • Believes each case runs in its own goroutine
open as a page

Why does a Go for-select loop keep processing work after its `ctx.Done()` case is ready?

level: middleimportance: should knowfreq 54%

basics

~20 s

Select picks uniformly at random among the cases that can proceed; source order gives no preference. A cancelled context's Done channel is ready, but so is the work channel, so each pass is a coin flip.

open as a page

In a Go for-select loop, what do you check before approving a pull request that adds a new case?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Check that the new case's body never blocks, that it either returns or falls through to the next pass, and that adding it makes the cancellation case rarer, since the choice is uniform among ready cases.

open as a page

When does Go evaluate the channel and send-value expressions of a `select` statement's cases?

level: middleimportance: nice to knowfreq 27%

basics

~20 s

Go evaluates every case's channel expression, and every send case's value, exactly once in source order on entering the select, before any case is chosen. A call in a case header therefore runs on every pass, even when its case does not win.

open as a page