select & Multiplexing Channels

select lets one goroutine wait on several channel operations at once and act on whichever becomes ready first — the tool you use to add timeouts, non-blocking checks, cancellation, and to merge many channels into one.

Learn select & Multiplexing Channels in our free Go course — an interactive lesson with runnable examples, a practice exercise and a quick reference.

Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ select over multiple channels

A select block lists several channel operations as case s. It blocks until one of them can proceed, then runs that single case. If more than one is ready it chooses at random — so to keep an example deterministic we arrange for exactly one case to be ready at a time. Below, pick runs one select per call, and we feed the channels in a fixed order.

2️⃣ The default case: non-blocking

Add a default and select stops waiting: if no other case can run right now , default runs immediately. This gives you a non-blocking receive ("is there a value? if not, carry on") and a non-blocking send ("try to send; if the buffer is full, drop it"). It's the building block for polling and back-pressure.

3️⃣ Timeouts and a done channel

Two of the most common select patterns. A timeout races your real result against time.After(d) , a channel that fires after duration d — whichever arrives first wins. A done channel signals cancellation: you close it, and because a closed channel makes every <-done return instantly, any number of goroutines can be told to stop with a single close(done) .

Fan-in goes one step further: it merges several channels into one. Each input is copied across by its own goroutine, and a WaitGroup closes the merged output when they're all drained. Because the goroutines race, we collect the values and sort them for a stable, printable result.

🎯 Your Turn

Wire up a result-vs-timeout race. Fill in the two blanks marked ___ using the hints, then run it.

❌ Relying on case order. Writing two ready cases and expecting the first to run.

✅ select picks at random among ready cases. Make one ready at a time, or collect-and-sort the outputs.

❌ Busy-waiting with for &#123; select &#123; default: &#125; &#125; and no real case — pins a CPU at 100%.

✅ Add a blocking case (a real channel or time.After ) so the loop parks instead of spinning.

❌ Leaking timers: case <-time.After(d) inside a hot loop allocates a fresh timer each pass.

✅ Create one time.NewTimer outside the loop and Reset it, or use context.WithTimeout .

❌ Sending on a channel to cancel many goroutines — only one receiver wakes per send.

✅ Use close(done) : a closed channel unblocks every waiting <-done at once.

time.After(0) isn't ready the instant select runs, so default ("b") usually wins. select never waits when a default is present.

Both receives return immediately with the zero value. A closed channel never blocks a receiver — which is exactly why closing a done channel cancels many goroutines at once.

Drain a buffered channel without ranging over it: loop with a select that has a receive case and a default . When default fires, the channel is empty — print drained and break.

Practice quiz

What does a select statement do?

  • Loops over a channel's values
  • Closes all listed channels
  • Waits on several channel operations and runs the one that is ready
  • Sorts channel values

Answer: Waits on several channel operations and runs the one that is ready. select is a switch for channels: it blocks until a case can proceed, then runs it.

If several select cases are ready at the same instant, which one runs?

  • One chosen at random
  • The first listed case
  • The last listed case
  • All of them in order

Answer: One chosen at random. Go picks a ready case at random to prevent starvation.

What does adding a default case to a select do?

  • Makes it block forever
  • Repeats the select
  • Closes the channel
  • Makes it non-blocking — default runs if no other case is ready

Answer: Makes it non-blocking — default runs if no other case is ready. With a default, select never waits: if nothing else is ready, default runs immediately.

How do you add a timeout to a channel operation in a select?

  • case <-time.Sleep(d)
  • case <-time.After(d)
  • default: time.After(d)
  • case timeout(d)

Answer: case <-time.After(d). time.After(d) returns a channel that fires after d; race it against your real case.

Why close a done channel instead of sending on it to cancel goroutines?

  • A closed channel makes every <-done receive return immediately, cancelling many goroutines at once
  • Closing is faster to type
  • Sending is not allowed on done channels
  • Closing sends a value to each receiver

Answer: A closed channel makes every <-done receive return immediately, cancelling many goroutines at once. A closed channel unblocks every waiting receiver; a send reaches only one.

What happens with an empty select {} that has no cases?

  • It returns immediately
  • It runs default
  • It blocks forever (deadlock if nothing else runs)
  • It panics at compile time

Answer: It blocks forever (deadlock if nothing else runs). select {} has nothing it can proceed on, so it blocks forever.

A receive from a closed channel in a select case does what?

  • Blocks until reopened
  • Returns immediately with the zero value
  • Panics
  • Skips the case

Answer: Returns immediately with the zero value. Receiving from a closed channel returns the zero value without blocking.

What is the risk of for { select { default: } } with no other case?

  • A deadlock
  • A compile error
  • A memory leak only
  • A busy-wait that pins a CPU at 100%

Answer: A busy-wait that pins a CPU at 100%. With only a default the loop never parks, spinning and burning a CPU core.

What does select with a default offer for a send like 'case out <- i'?

  • A guaranteed send
  • A non-blocking try-send that hits default if the buffer is full
  • A blocking send forever
  • An automatic retry

Answer: A non-blocking try-send that hits default if the buffer is full. default makes the send non-blocking: if it can't proceed, default runs (a try-send).

What is fan-in?

  • Splitting one channel into many
  • Sorting a channel
  • Merging several input channels into one output channel
  • Closing channels in order

Answer: Merging several input channels into one output channel. Fan-in merges multiple input channels into a single output channel.