Free — no signup required

Concurrency: Select and Context Timeouts

3 min read

Managing Multiple Channels and Deadlines

In production Ops tools, you cannot wait forever. A health check against an unreachable server should time out after 5 seconds, not block indefinitely. A deployment script that calls an API should give up if the API is unresponsive. You need a way to race a real operation against a deadline.

The select Statement

select is like a switch statement, but for channel operations. It waits until one of its cases can proceed, then executes that case. If multiple cases are ready simultaneously, it picks one at random.

select {
case msg := <-ch1:
    fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
    fmt.Println("Received from ch2:", msg)
default:
    fmt.Println("No channel ready, moving on")
}

The default case makes select non-blocking — if no channel is ready, it falls through immediately.

The context Package

The context package is Go's standard mechanism for propagating deadlines, timeouts, and cancellation signals across goroutines and API boundaries. Every major Go library — the HTTP client, database drivers, gRPC — accepts a context.Context as its first argument.

The pattern:
1. Create a context with a deadline: context.WithTimeout(parent, duration)
2. Pass the context into functions that do work
3. Those functions use select to listen on ctx.Done() — a channel that closes when the deadline expires or the context is cancelled
4. Always defer cancel() immediately after creating a context to release resources

package main

import (
    "context"
    "fmt"
    "time"
)

func fetchMetrics(ctx context.Context, host string) (string, error) {
    resultChan := make(chan string, 1)

    go func() {
        // Simulate a slow metrics endpoint
        time.Sleep(3 * time.Second)
        resultChan <- fmt.Sprintf("%s: cpu=42%%", host)
    }()

    select {
    case result := <-resultChan:
        return result, nil
    case <-ctx.Done():
        // ctx.Err() returns context.DeadlineExceeded or context.Canceled
        return "", fmt.Errorf("metrics fetch for %s: %w", host, ctx.Err())
    }
}

func main() {
    // 1-second timeout; the fetch takes 3 seconds
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    result, err := fetchMetrics(ctx, "prometheus-01")
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)
}

Output:

Error: metrics fetch for prometheus-01: context deadline exceeded

Context cancellation propagates. If you create a child context from a parent, cancelling the parent automatically cancels all children. This means you can cancel an entire tree of in-flight operations — parallel API calls, database queries, sub-goroutines — with a single cancel() call. This is how Kubernetes controllers and operators handle graceful shutdown.

Interview Tip

A common interview question is: "How do you handle timeouts in Go?" The answer should mention context.WithTimeout, passing the context as the first argument to all downstream calls, using select with ctx.Done() to detect expiry, and always calling defer cancel() to prevent context leaks. Bonus points for mentioning that ctx.Err() distinguishes between DeadlineExceeded and Canceled, which matters for error reporting and retry logic.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free