Skip to content
Nexonauts
Go by example

Go by example

Select

Wait on multiple channel operations at once.

select is the multiplexer. It looks like a switch, but each case is a channel operation, and the runtime picks one of the cases whose channel is ready. If several are ready at the same time, it picks pseudo-randomly. If none are ready, select blocks until one becomes ready — unless you provide a default case, in which case select returns immediately.

This is the primitive that makes Go's concurrency feel composable. Combine it with time.After and you get timeouts. Combine it with a "done" channel and you get cancellation. Combine it with default and you get non-blocking sends or receives.

The two-channel case

Two goroutines produce values on different schedules. select waits for whichever fires first.

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		time.Sleep(100 * time.Millisecond)
		ch1 <- "from ch1"
	}()
	go func() {
		time.Sleep(200 * time.Millisecond)
		ch2 <- "from ch2"
	}()

	for i := 0; i < 2; i++ {
		select {
		case msg := <-ch1:
			fmt.Println(msg)
		case msg := <-ch2:
			fmt.Println(msg)
		}
	}
}

Timeout

time.After(d) returns a channel that delivers a value after d. Put it in a select and you have a hard timeout for any other channel op in the same select.

select {
case res := <-doWork():
	fmt.Println("done:", res)
case <-time.After(1 * time.Second):
	fmt.Println("timed out")
}

If doWork() returns within a second, you get the result. If not, the second case fires and the goroutine moves on. The work goroutine itself may still be running — select only stops waiting; cancellation is separate (typically context.Context).

Non-blocking with default

A default case turns select into a poll. Use it sparingly — it's almost always a sign you're trying to fake what a buffered channel or a worker goroutine should be doing.

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

Done channels and cancellation

A common pattern: workers receive on done to know when to stop. select lets them watch done and do their actual work in the same loop.

func worker(done <-chan struct{}, jobs <-chan int) {
	for {
		select {
		case j := <-jobs:
			process(j)
		case <-done:
			return
		}
	}
}

chan struct{} is the zero-cost signal channel — struct{} carries no data, so the channel is effectively just "is anyone closing this?"

Run it

$ go run select.go
from ch1
from ch2