Code of the Day
AdvancedConcurrency

The select statement

Wait on multiple channels with select — default for non-blocking ops, timeouts with time.After, and fan-in patterns.

GoAdvanced11 min read
Recommended first
By the end of this lesson you will be able to:
  • Use select to wait on multiple channel operations simultaneously
  • Add a default case to make a select non-blocking
  • Implement a timeout with time.After inside select
  • Describe the fan-in pattern and when to use it
  • Use a done channel to signal goroutine shutdown

Channels let two goroutines communicate. But what if you need to wait on several channels and act on whichever is ready first? That is what does. It is to channels what a switch is to values — except the cases are channel operations, and the choice is made at runtime based on which operation can proceed.

Basic select

select {
case v := <-ch1:
    fmt.Println("received from ch1:", v)
case v := <-ch2:
    fmt.Println("received from ch2:", v)
}

select blocks until one case can proceed. If multiple cases are ready simultaneously, Go picks one at random. This randomness is intentional — it prevents one channel from starving others.

You can also send in a select case:

select {
case ch <- value:
    fmt.Println("sent")
case result := <-done:
    fmt.Println("done:", result)
}

The default case — non-blocking operations

Add default to make a select non-blocking:

select {
case v := <-ch:
    fmt.Println("got:", v)
default:
    fmt.Println("no value ready, moving on")
}

Without default, select blocks. With default, it executes the default case immediately if no channel operation is ready. This is the Go pattern for polling a channel without waiting.

A select with only a default case and a blocking channel operation in a tight loop burns CPU. If you are polling, add a small time.Sleep or restructure to block properly. Pure spin-loops defeat the purpose of cooperative scheduling.

Timeouts with time.After

time.After(d) returns a channel that receives a time.Time value after duration d. Combine it with select for a timeout:

select {
case result := <-work:
    fmt.Println("finished:", result)
case <-time.After(5 * time.Second):
    fmt.Println("timed out after 5s")
}

This is the idiomatic Go timeout pattern. It is much simpler than setting OS-level deadlines directly.

In production code, prefer using a context.Context with WithTimeout rather than time.After directly. Context cancellation propagates through the call stack, while time.After channels are local. We cover context in the next lesson.

The done channel idiom

A "done" channel signals goroutines to stop:

done := make(chan struct{})

go func() {
    for {
        select {
        case <-done:
            fmt.Println("shutting down")
            return
        case v := <-work:
            process(v)
        }
    }
}()

// Later, to stop the goroutine:
close(done)

chan struct{} uses zero bytes of memory — it is idiomatic for signals that carry no data, only presence. Closing done unblocks all goroutines waiting on it simultaneously.

Fan-in: merging multiple channels

A fan-in function multiplexes multiple input channels into one output channel:

func merge(cs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    for _, c := range cs {
        wg.Add(1)
        go func(ch <-chan int) {
            defer wg.Done()
            for v := range ch {
                out <- v
            }
        }(c)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

Each input channel gets its own goroutine that forwards values to the shared out channel. When all input channels are closed and all goroutines finish, out is closed. The consumer reads from out without knowing how many sources there are.

Check your understanding

Knowledge check

  1. 1.
    Two cases in a select statement are both ready at the same time. What does Go do?
  2. 2.
    You want to check if a channel has a value ready without blocking. Which construct do you use?
  3. 3.
    Closing a done channel is the idiomatic way to broadcast a stop signal to multiple goroutines simultaneously.

Do it yourself

Write a program that launches two slow goroutines (each sleeping for a random duration 1-3 s). Use select and time.After(2 * time.Second) to print whichever finishes first, or "timeout" if neither finishes in time.

go run main.go
go vet ./...

Where to go next

select is powerful but not enough on its own for all synchronisation needs. The next lesson covers sync primitivesMutex, WaitGroup, and sync.Once — for when channels are not the right tool.

Finished reading? Mark it complete to track your progress.

On this page