Skip to content

select

A select statement waits on multiple channel operations at once and proceeds with whichever is ready first. It's the control structure that makes channels composable — timeouts, cancellation, and multiplexing all build on it.

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

Each case is a send or receive. select blocks until one can proceed, runs that case, and continues. If several are ready at once, it picks one at random — so don't rely on case order for priority.

c1 := make(chan string, 1)
c2 := make(chan string, 1)
c1 <- "hello"            // only c1 has a value

select {
case m := <-c1:
    fmt.Println(m)       // output: hello
case m := <-c2:
    fmt.Println(m)
}

default: don't block

A default case runs immediately if no other case is ready, turning select into a non-blocking operation. This is how you poll a channel or do a non-blocking send.

ch := make(chan int)     // empty, no sender

select {
case v := <-ch:
    fmt.Println("got", v)
default:
    fmt.Println("nothing ready")   // output: nothing ready
}

Timeouts with time.After

time.After(d) returns a channel that delivers a value after duration d. Put it in a select and you get a timeout for free:

ch := make(chan int)     // nobody will send

select {
case v := <-ch:
    fmt.Println("got", v)
case <-time.After(10 * time.Millisecond):
    fmt.Println("timeout")          // output: timeout
}

The for-select loop with a done channel

The workhorse pattern for a long-running goroutine: loop on select, handling work on one channel and a stop signal on another. A closed channel makes every receive return immediately, so it broadcasts "stop" to all listeners.

logs := make(chan string)
shutdown := make(chan struct{})
finished := make(chan struct{})

go func() {                          // a background logger
    for {
        select {
        case line := <-logs:
            fmt.Println("LOG:", line)
        case <-shutdown:
            fmt.Println("logger stopped")
            close(finished)
            return
        }
    }
}()

logs <- "server started"
logs <- "request handled"
close(shutdown)          // tell the logger to stop
<-finished               // wait for it to actually exit
// output:
// LOG: server started
// LOG: request handled
// logger stopped

Disabling a case with a nil channel

A receive (or send) on a nil channel blocks forever, so setting a channel variable to nil removes its case from a select. This is the idiomatic way to stop listening on a channel mid-loop without restructuring the select.

var ch chan int          // nil
select {
case <-ch:               // never fires — ch is nil
    fmt.Println("unreachable")
case <-time.After(time.Millisecond):
    fmt.Println("only this can happen")   // output: only this can happen
}

An empty select blocks forever

select {} with no cases blocks the goroutine permanently — occasionally used to park main while background goroutines run, but in main with nothing else runnable it triggers the deadlock detector:

func main() {
    select {}   // no cases — block forever
}
// fatal error: all goroutines are asleep - deadlock!

Quick reference

Form Meaning
select { case ...: } wait until one channel op is ready
several ready one chosen at random
default: run if nothing else ready (non-blocking)
case <-time.After(d): timeout branch
for { select { ... } } long-running multiplexed loop
case <-done: stop signal (closed channel broadcasts)
nil channel case disabled (blocks forever)

Sources