The select Statement

intermediate go concurrency

select lets a goroutine wait on multiple channel operations at the same time. It blocks until one of them is ready, then runs that case.

In simple language, think of it like a switch statement — but instead of comparing values, each case is a channel send or receive, and it picks whichever one is ready first.

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

This sits and waits. The moment either ch1 or ch2 has something, that case runs and the rest are ignored.

select waits on all channels, fires the first ready one
ch1 ch2 ch3
select first ready case runs

What if several are ready at once?

Go picks one at random. This is deliberate — it stops any single channel from starving the others. We can’t rely on ordering, so never assume the first case wins.

Non-blocking with default

Add a default case and select stops blocking. If nothing is ready right now, it runs default immediately.

select {
case msg := <-ch:
	fmt.Println("received:", msg)
default:
	fmt.Println("nothing ready, moving on") // runs instantly if ch is empty
}

This is the classic “try to receive but don’t wait around” pattern.

The timeout pattern

This is the one interviewers love. We pair a real channel against time.After, which returns a channel that fires after a duration.

select {
case result := <-ch:
	fmt.Println("got result:", result)
case <-time.After(2 * time.Second):
	fmt.Println("timed out!") // ch took too long
}

If ch doesn’t deliver within 2 seconds, the timeout case wins. Dead simple way to avoid waiting forever.

The done-channel pattern

We can tell a goroutine to stop by closing a done channel. Since a closed channel always returns immediately, the <-done case fires the moment we close it.

func worker(jobs <-chan int, done <-chan struct{}) {
	for {
		select {
		case j := <-jobs:
			fmt.Println("working on", j)
		case <-done: // closed channel unblocks instantly
			fmt.Println("stopping")
			return
		}
	}
}

We close done from outside and the worker exits cleanly. (In real code we’d usually reach for context here — covered next.)

Interview soundbite

select is like a switch for channels — it blocks until one of several channel operations is ready, and if several are ready it picks one at random to prevent starvation. Add a default case to make it non-blocking. The two patterns to know cold are the timeout (case <-time.After(...)) and the done-channel for cancellation.