iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…