select Statement & Default Cases
select lets one goroutine wait on multiple channel operations at once.
Add default when you need a non-blocking probe, timeout, or tight loop with other work.
Summary
select blocks until at least one case can proceed: send, receive, or default.
If several cases are ready, Go chooses one pseudo-randomly.
default runs immediately when no channel case is ready - never use it in a spin loop without time.Sleep or runtime.Gosched.
Timeouts combine select with time.After or context.Context deadlines.
Recipe
Quick-reference recipe card - copy-paste ready.
select {
case msg := <-messages:
handle(msg)
case err := <-errors:
log(err)
case <-time.After(5 * time.Second):
return fmt.Errorf("timeout")
default:
// non-blocking: no channel ready right now
}When to reach for this:
- Multiplex results, errors, and done signals in one loop.
- Implement timeouts around blocking receives.
- Disable cases dynamically by setting channel vars to
nil. - Avoid
defaultbusy loops in servers - use blockingselector separate goroutines.
Working Example
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string, 1)
quit := make(chan struct{})
go func() {
time.Sleep(200 * time.Millisecond)
ch <- "result"
}()
for {
select {
case v := <-ch:
fmt.Println("received", v)
close(quit)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
close(quit)
case <-quit:
fmt.Println("shutting down loop")
return
}
}
}What this demonstrates:
- Work and timeout compete in one
select. quitchannel breaks the loop after first outcome.- Buffered
chlets the sender complete without a concurrent receiver at send instant (still works unbuffered with paired goroutine). - Production code prefers
context.WithTimeoutover rawtime.Afterin long-lived loops (timer GC - see Gotchas).
Deep Dive
How It Works
selectevaluates channel operations like standalone sends/receives but waits on the first ready case.- All channel expressions are evaluated once at
selectentry (including send value expressions). defaultbypasses blocking when no case is immediately ready.- Setting a case's channel to
nildisables that branch until reassigned. for { select { ... } }is the idiomatic event loop for channel-driven goroutines.
select Cases at a Glance
| Case form | Blocks when |
|---|---|
case x := <-ch | No value available (unless default) |
case ch <- v | Channel full (buffered) or no receiver (unbuffered) |
case <-ctx.Done() | Context not canceled |
default | Never blocks |
Timeout Patterns
| Pattern | Notes |
|---|---|
time.After(d) | Simple; allocate timer per iteration in loops |
context.WithTimeout | Preferred in HTTP/gRPC handlers |
time.NewTimer + Stop | Reuse timer in hot loops |
Go Notes
// Disable a case by nil channel
var ch chan int // nil
select {
case v := <-ch: // never selected
_ = v
default:
fmt.Println("ch disabled")
}
// Non-blocking send
select {
case out <- item:
default:
return ErrQueueFull
}Gotchas
time.Afterin tight loops - Creates timers that GC may delay freeing. Fix:time.NewTimer, reset carefully, orcontext.defaultbusy spin - Burns CPU. Fix: block on channels or sleep/yield between polls.- Assuming select priority - Ready cases are chosen randomly. Fix: separate loops or serialized handling if order matters.
- Send side evaluated eagerly -
case ch <- expensive()runsexpensive()before waiting. Fix: compute beforeselector use inner goroutine. breakonly exitsselect- Not the surroundingfor. Fix: use labels orreturnto leave the loop.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Blocking single receive | One input channel | Multiple sources + timeout |
| Separate goroutine per source | Simple fan-in | Many sources (combine with select) |
errgroup + context | Task errors and cancel | Fine-grained channel events |
Poll with default | Rare status checks | High-frequency server loops |
FAQs
Does select choose the first case in source order?
No - if multiple cases are ready, selection is pseudo-random.
Do not depend on textual order for priority.
When should I use default?
For non-blocking try-send/receive or quick probes.
Not for spinning until data arrives.
Can select wait on the same channel twice?
Duplicate cases on one channel are useless - only one can run.
Use different channels or sequential receives.
How do I break out of for-select?
Use return, a done channel case, or break with a label on the for loop.
Is select fair under load?
Roughly fair among ready cases but not a priority scheduler.
Design explicit priority with separate goroutines if needed.
Can I select on nil channels?
Nil channel operations never proceed, so those cases are skipped unless all are nil (deadlock).
How do HTTP servers use select?
Often indirectly via net/http and context.
Custom loops multiplex shutdown signals, work queues, and tickers.
What about select with empty default?
default: alone makes a non-blocking select - useful for try-ops, harmful in tight loops.
Does context.Done work in select?
Yes - <-ctx.Done() is idiomatic for cancellation alongside work channels.
How is select different from switch?
switch compares values; select waits on channel readiness.
Both choose one branch but solve different problems.
Related
- Concurrency Basics - intro select example
- Channels: Buffered, Unbuffered & Closing Rules - channel semantics
- Concurrency in Go: Goroutines and Channels First - multiplexing in CSP
- Goroutines: Creation Cost & Scheduling Overview - event loops at scale
- Concurrency Fundamentals Best Practices - timeout and loop habits
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).