Select Statement
Learn how the select statement waits on multiple channels at once, and how the default case enables non-blocking channel operations.
Introduction
Real concurrent programs often need to wait on more than one channel at a time — maybe a result channel and a cancellation channel, or several worker outputs. The select statement is Go's way of expressing exactly that: "wait until one of these channel operations is ready, then act on it."
- The basic syntax of a select statement.
- How select picks randomly among multiple ready cases.
- How a default case makes channel operations non-blocking.
- How to build a timeout using select and time.After.
Basic select Syntax
select looks like a switch statement, but each case is a channel send or receive operation. select blocks until one of its cases can proceed; if more than one is ready at the same time, Go picks one at random.
package main
import ( "fmt" "time")
func main() { ch1 := make(chan string) ch2 := make(chan string)
go func() { time.Sleep(50 * time.Millisecond) ch1 <- "result from ch1" }()
go func() { time.Sleep(20 * time.Millisecond) ch2 <- "result from ch2" }()
for i := 0; i < 2; i++ { select { case msg1 := <-ch1: fmt.Println(msg1) case msg2 := <-ch2: fmt.Println(msg2) } }}Click Run to see what this code prints.
ch2's goroutine sleeps for less time, so it becomes ready first and select handles that case first. The loop then waits again and picks up ch1 once it becomes ready too.
select With a default Case
Adding a default case makes select non-blocking: if no other case is ready immediately, default runs right away instead of waiting.
package main
import "fmt"
func main() { ch := make(chan string)
select { case msg := <-ch: fmt.Println("received:", msg) default: fmt.Println("no message ready, moving on") }}Click Run to see what this code prints.
This pattern is useful for polling a channel without blocking the calling goroutine — for example, checking for a shutdown signal on every loop iteration without stopping to wait for it.
Timeouts with select and time.After
A very common use of select is implementing a timeout: race a real channel operation against time.After, which returns a channel that fires once after the given duration.
package main
import ( "fmt" "time")
func slowOperation(ch chan string) { time.Sleep(200 * time.Millisecond) ch <- "operation finished"}
func main() { ch := make(chan string) go slowOperation(ch)
select { case result := <-ch: fmt.Println(result) case <-time.After(100 * time.Millisecond): fmt.Println("timed out waiting for operation") }}Click Run to see what this code prints.
Since slowOperation takes 200ms but the timeout is only 100ms, the time.After case wins the race. Flip the numbers and the real result would print instead. This pattern is the backbone of most timeout and cancellation logic in Go network code.
Common Mistakes
- Adding a default case when you actually want select to block and wait — this turns it into a busy loop instead.
- Forgetting that select picks randomly among ready cases, and relying on a specific case winning.
- Calling time.After inside a loop repeatedly, leaking timers that only get cleaned up after they fire.
- Using select with only one case, when a plain channel receive would be clearer.
- Assuming select checks cases in the order they are written — it does not.
Best Practices
- Use select without default when you genuinely want to block until something happens.
- Use select with default for non-blocking polling or a quick "is anything ready?" check.
- Combine select with a context.Context Done() channel for clean cancellation.
- Prefer a reusable time.NewTimer over time.After inside loops to avoid timer leaks.
- Keep each select case short — do the real work outside the select body when it is more than a line or two.
Frequently Asked Questions
It blocks until one of its cases becomes ready. If none ever will and there is no default, it blocks forever (a deadlock if nothing else in the program can unblock it).
Yes, a case can be either a send (ch <- value) or a receive (v := <-ch); select works with both.
It picks one uniformly at random among all cases that are currently ready, to avoid starving any particular channel.
Key Takeaways
- select waits on multiple channel operations and proceeds with whichever becomes ready first.
- If several cases are ready simultaneously, select chooses one at random.
- A default case makes select non-blocking, running immediately if nothing else is ready.
- select combined with time.After is the standard way to implement timeouts.
- select is central to writing responsive, cancellable concurrent code in Go.
Summary
select gives you a clean way to coordinate multiple channels, whether you are racing several sources for the first result, polling non-blockingly, or enforcing a timeout. Next, you will look at the sync package, which offers lower-level tools like Mutex and WaitGroup for protecting shared state directly.