In Go, what does a `select` statement do that a plain channel receive like `v := <-ch` cannot?
answer
- one goroutine, several channels
- it waits for the first thing possible
- cases are operations, not values
- only one case body runs per pass
- ties are broken at random, not by order
basics
~20 sA 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 sA `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// 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
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.
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.
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.
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