select
Go’s select picks ONE ready channel operation. Used to multiplex on multiple channels, implement timeouts, default branches for non-blocking I/O.
Send, receive, timeout, default
EXAMPLE
package main
import (
"context"
"fmt"
"time"
)
// 1) Basic select — wait on multiple channels
func main() {
a := make(chan int)
b := make(chan int)
go func() { time.Sleep(1 * time.Second); a <- 42 }()
go func() { time.Sleep(2 * time.Second); b <- 99 }()
select {
case v := <-a:
fmt.Println("from a:", v) // wins; runs first
case v := <-b:
fmt.Println("from b:", v)
}
}
// 2) Timeout pattern
func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
ch := make(chan string, 1)
go func() {
result, _ := slowFetch(url)
ch <- result
}()
select {
case r := <-ch:
return r, nil
case <-time.After(timeout):
return "", fmt.Errorf("timeout after %v", timeout)
}
}
// 3) Default branch — non-blocking
func tryReceive(ch <-chan int) {
select {
case v := <-ch:
fmt.Println("got:", v)
default:
fmt.Println("nothing ready")
}
}
// Non-blocking send
select {
case ch <- 42:
fmt.Println("sent")
default:
fmt.Println("channel full or no receiver")
}
// 4) Context cancellation — the idiomatic pattern
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
return // cancelled
case job, ok := <-jobs:
if !ok { return } // channel closed
process(job)
}
}
}
// Caller:
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
go worker(ctx, jobs)
// 5) Fan-in — merge multiple channels
func fanIn(ctx context.Context, chs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chs))
for _, c := range chs {
go func(c <-chan int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case v, ok := <-c:
if !ok { return }
select {
case out <- v:
case <-ctx.Done():
return
}
}
}
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}
// 6) Heartbeat / progress
func longJob(ctx context.Context, out chan<- string) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
out <- "heartbeat"
case <-time.After(10 * time.Second): // total work duration
out <- "done"
return
}
}
}
// 7) Rate limit — limit operations per second
func rateLimited(requests []Request) {
limit := time.Tick(200 * time.Millisecond) // 5 / second
for _, req := range requests {
<-limit // block until next tick
go process(req)
}
}
// 8) Burst rate limit (token bucket)
func burstLimit(requests []Request, capacity int, refillRate time.Duration) {
burst := make(chan time.Time, capacity)
go func() {
for {
burst <- time.Now()
time.Sleep(refillRate)
}
}()
for _, req := range requests {
<-burst
go process(req)
}
}
// 9) First-result wins — race two providers
func raceProviders(ctx context.Context) (string, error) {
a := make(chan string, 1)
b := make(chan string, 1)
go func() { a <- fetchA() }()
go func() { b <- fetchB() }()
select {
case r := <-a:
return r, nil
case r := <-b:
return r, nil
case <-ctx.Done():
return "", ctx.Err()
}
}
// 10) nil channel — disables the case
func selective() {
var ch chan int // nil — never ready
select {
case v := <-ch: // case is always disabled
fmt.Println(v)
case <-time.After(1 * time.Second):
fmt.Println("timeout — ch was nil")
}
}
// Useful pattern: enable / disable a case at runtime
func producer(send chan<- int, stopAt int) {
var out chan<- int = send
val := 0
for {
select {
case out <- val:
val++
if val >= stopAt {
out = nil // disable further sends
}
case <-time.After(5 * time.Second):
return // exit if idle
}
}
}
// 11) Multiple cases ready — random selection
func randomChoice() {
a := make(chan int, 1); a <- 1
b := make(chan int, 1); b <- 2
select {
case v := <-a: fmt.Println("a:", v)
case v := <-b: fmt.Println("b:", v)
}
// Random which one wins — Go shuffles ready cases.
}
// 12) Real-world: graceful HTTP server shutdown
func main() {
srv := &http.Server{Addr: ":8080", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
select {
case sig := <-quit:
log.Printf("received %v, shutting down", sig)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
}
// 13) Pipeline cancellation
func stage(ctx context.Context, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in {
select {
case <-ctx.Done():
return // exit cleanly
case out <- v * 2:
}
}
}()
return out
}
// 14) Loop with select — common idiom
for {
select {
case <-ctx.Done():
return
case msg := <-incoming:
handle(msg)
case <-time.After(30 * time.Second):
log.Println("idle for 30s, exiting")
return
}
}
// 15) Common bugs
// • Forgetting `<-ctx.Done()` → goroutine leaks when caller cancels
// • select with only one case (no default) → identical to blocking receive (use the receive directly)
// • Default in a hot loop → 100% CPU spinning
// • Selecting on a nil channel without disabling logic → permanently blocked case (sometimes desired!)
// • Sending in select to a possibly-closed channel → panic; check ok or use mutex
// • Multiple time.After in long-running loops → leaks timers; use NewTicker + defer Stop
// 16) Performance tips
// • Reusable Timer via NewTimer() + Reset(), not time.After() in hot loops
// • Buffered channels avoid unnecessary blocking
// • Consider errgroup for goroutine coordination + error propagation
// • Don't over-multiplex; a goroutine doing 10 things via select can be confusing
// 17) When to NOT use select
// • Single channel — just receive directly
// • Coordinating function calls — use errgroup or sync.WaitGroup
// • Need ordered processing — don't use select (it's non-deterministic)
// 18) Best practices
// ✅ Always include <-ctx.Done() in long-running selects
// ✅ Use time.NewTimer + Stop instead of time.After in hot loops
// ✅ Pair with for { select { ... } } for event loops
// ✅ nil channels disable cases; use to skip arms conditionally
// ✅ Errgroup / WaitGroup for parallel work; select for multiplexing
// ✅ Test cancellation paths — they're often the bug source
// ✅ Document which branch handles what — select cases can be subtle
Why it matters
select + context.Done is the goroutine-lifetime pattern; without it, cancelling a context leaks workers. Pair with errgroup for parallel work, NewTimer (not time.After) inside hot loops, and nil channels to disable cases dynamically.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
select {
case v := <-c1: fmt.Println("c1", v)
case v := <-c2: fmt.Println("c2", v)
case <-time.After(time.Second): fmt.Println("timeout")
}
Try it Yourself »
Discussion
Loading…