skip to content

default and Non-Blocking Operations

Adding a default case turns select into a non-blocking attempt: try to send, and if nobody is ready, drop the value or move on. Interviewers like it for load-shedding designs and then ask why a bare for-select-default loop pegs a CPU.

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

questions

4

What does a `default` case do in a Go `select` statement?

level: juniorimportance: must knowfreq 68%

answer

  1. the case that never waits
  2. what happens when nothing is ready
  3. no default means the goroutine parks
  4. not a fallback, and not a timeout
  5. chosen only if no case can proceed now

basics

~20 s

A default case makes the select non-blocking. If no other case can proceed at that instant, Go runs default immediately instead of waiting. Without a default, the select parks the goroutine until one of its cases is ready.

solid answer

~40 s

`select` normally blocks: it waits until one of its channel operations can proceed, and if several are ready it picks one at random. Adding a `default` case changes that completely. The runtime still evaluates every case to see whether it can proceed right now; if at least one can, that case is chosen exactly as usual and `default` is ignored. If none can, `default` runs and the select returns immediately, so the goroutine never parks. That is the entire meaning of "non-blocking" here: `default` is not a fallback tried after the others fail, and it does not wait even for an instant. A select may contain at most one `default`, and its position in the source is irrelevant, since cases are not evaluated top to bottom like a `switch`.

code

go · 11 lines
go
// blocks until a value arrives on ch
v := <-ch
use(v)

// never blocks: takes a value only if one is ready right now
select {
case v := <-ch:
	use(v)
default:
	// nothing was ready; carry on without a value
}

go deeper

for a junior

Be ready to state the one-line rule: with a default, the select never blocks; without one, the goroutine waits. Know that at most one default is allowed.

for a middle

An interviewer expects you to explain the evaluation order: readiness is checked across all cases first, a random ready case wins, and only a completely unready select reaches default. Say why that is not switch semantics.

for a senior

Show that you know the operational price. A blocking select is free while idle; a default inside a bare loop pins a core. Be able to say when you would reach for the non-blocking form at all.

for a principal

Frame it as a policy choice for a codebase: non-blocking probes make dropped work invisible, so teams should agree where a try-send is allowed and require a counter on every drop path.

## What `select` does without a `default` A `select` statement lists several channel operations — sends, receives, or both — and performs exactly one of them. The runtime looks at all the cases and asks, for each, "can this communication proceed right now?" A receive can proceed if a sender is waiting, if the channel's buffer holds a value, or if the channel is closed. A send can proceed if a receiver is waiting or the buffer has room. - If **exactly one** case can proceed, it is chosen. - If **several** can proceed, one is chosen **uniformly at random** — not by source order. - If **none** can proceed, the select **blocks**: the goroutine is parked and consumes no CPU until one of the channels becomes ready. That last line is the default behaviour and it is usually what you want. A parked goroutine is cheap; it costs a little memory and nothing else. ## What adding `default` changes ```go select { case v := <-ch: use(v) default: // nothing was ready this instant } ``` The first two bullets above are unchanged. Only the third is replaced: instead of parking the goroutine, the runtime executes the `default` body and the select finishes. The select becomes a **non-blocking probe** — an attempt that either succeeds now or gives up now. Three points people get wrong: 1. **`default` is not a fallback for the other cases.** It does not mean "if the receive fails, do this instead". The receive never "fails"; it is either ready or not ready, and only *not ready* reaches `default`. 2. **`default` does not wait.** There is no grace period, no retry, no short timeout. If you want a timeout you need a timer channel as one of the *cases*, not a `default`. 3. **`default` does not change the random choice.** If two real cases are both ready, `default` has no effect at all on which one runs. ## Syntax rules A select may have **at most one** `default` clause; two is a compile-time error. Its position among the cases has no meaning — `default` written first behaves exactly like `default` written last, because cases are not scanned in order. An empty `select {}` with no cases and no default blocks forever; `select { default: }` with only a default is a no-op that returns immediately. ## Why it exists The non-blocking form buys you the ability to *make progress even when a communication is not possible*. Typical uses: - **Try-send**: offer a value to a channel and carry on if nobody can take it right now. - **Try-receive**: pick up work if any is waiting, otherwise do something else. - **Checking a signal without waiting for it**: probe a cancellation channel in the middle of a long computation without stopping the computation when it has not fired. ## The cost you must respect Because the select never waits, a `for` loop whose only select has a `default` case never yields to anything. It becomes a spin loop: the goroutine runs flat out, saturating one CPU core, doing no useful work while it waits for something to appear. A blocking select costs zero CPU while idle; a select with `default` inside a bare loop costs an entire core. The rule of thumb is that `default` belongs in code that is *already going to do something else* on this pass — not in a loop whose only purpose is to wait. ## A quick mental model Without `default`, the goroutine is a person sitting by the phone: asleep, costing nothing, woken when it rings. With `default`, the goroutine is a person walking past the phone, glancing at it, and continuing. Doing that once per pass through real work is fine. Doing it in a tight loop is how you burn a core to check an empty phone ten million times a second.

  • If two channel cases are ready and a default is present, which runs?
    One of the two ready cases, chosen uniformly at random. `default` is only reachable when no case can proceed, so it plays no part in the choice. Source order does not matter either; a select is not a `switch`.
  • How would you give a select a one-second timeout instead of a default?
    Add a timer channel as a real case, for example `case <-time.After(time.Second):`. That keeps the select blocking, so the goroutine parks and costs no CPU until either a real case becomes ready or the timer fires. A `default` would return instantly and never wait a second.
  • What does an empty `select {}` do?
    It blocks forever. There are no cases that could ever become ready and no default, so the goroutine parks permanently. If it is the last runnable goroutine, the runtime reports the fatal error "all goroutines are asleep - deadlock!".
  • Can a select have two default cases?
    No. At most one `default` is allowed and a second is a compile-time error. There would be nothing for a second one to mean, since `default` is not selected by order or condition — it simply runs when no communication is possible.

Without default, you wait at the mailbox until post arrives. With default, you glance at the mailbox on your way past and keep walking if it is empty.

saying these in an interview costs you the question

  • Says default runs after the other cases are tried in order
  • Thinks select waits briefly before falling to default
  • Describes default as a timeout mechanism
  • Claims a select with default still blocks until something is ready
  • Believes default changes which ready case is chosen
open as a page

How do you write a non-blocking send and a non-blocking receive on a Go channel?

level: middleimportance: should knowfreq 56%

basics

~20 s

Wrap the operation in a select with a default case: select { case ch <- v: default: } is a try-send, and select { case v := <-ch: default: } is a try-receive. Reaching default means the operation did not happen.

open as a page

A Go for-select loop with a default case pins a CPU core while the service is idle. How do you confirm it and fix it?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The default case makes the select return instantly, so the loop never waits and spins at full speed. Confirm it with a CPU profile of the idle process, then delete the default so the select blocks.

open as a page

Why does a select with a receive case and a default never pick default once the channel is closed?

level: middleimportance: nice to knowfreq 40%

basics

~20 s

A receive on a closed channel is always ready: it returns the element type's zero value with ok false, without waiting. Since the receive case can always proceed, the select takes it every time and default becomes unreachable.

open as a page