select Statement & Default Cases
select lets one goroutine wait on multiple channel operations at once.
Search across all documentation pages
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.
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.
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:
nil.default busy loops in servers - use blocking select or separate goroutines.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:
select.quit channel breaks the loop after first outcome.ch lets the sender complete without a concurrent receiver at send instant (still works unbuffered with paired goroutine).context.WithTimeout over raw time.After in long-lived loops (timer GC - see Gotchas).select evaluates channel operations like standalone sends/receives but waits on the first ready case.select entry (including send value expressions).default bypasses blocking when no case is immediately ready.nil disables that branch until reassigned.for { select { ... } } is the idiomatic event loop for channel-driven goroutines.| 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 |
| 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 |
// 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
}time.After in tight loops - Creates timers that GC may delay freeing. Fix: time.NewTimer, reset carefully, or context.default busy spin - Burns CPU. Fix: block on channels or sleep/yield between polls.case ch <- expensive() runs expensive() before waiting. Fix: compute before select or use inner goroutine.break only exits select - Not the surrounding for. Fix: use labels or return to leave the loop.| 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 |
No - if multiple cases are ready, selection is pseudo-random.
Do not depend on textual order for priority.
For non-blocking try-send/receive or quick probes.
Not for spinning until data arrives.
Duplicate cases on one channel are useless - only one can run.
Use different channels or sequential receives.
Use return, a done channel case, or break with a label on the for loop.
Roughly fair among ready cases but not a priority scheduler.
Design explicit priority with separate goroutines if needed.
Nil channel operations never proceed, so those cases are skipped unless all are nil (deadlock).
Often indirectly via net/http and context.
Custom loops multiplex shutdown signals, work queues, and tickers.
default: alone makes a non-blocking select - useful for try-ops, harmful in tight loops.
Yes - <-ctx.Done() is idiomatic for cancellation alongside work channels.
switch compares values; select waits on channel readiness.
Both choose one branch but solve different problems.
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).
Reviewed by Chris St. John·Last updated Jul 18, 2026