select and timeouts

Go · Practical · 10 min

select is how goroutines react to whichever event comes first: data, cancellation or a deadline.

A select statement waits on several channel operations and runs the case for whichever is ready first:

select {
case msg := <-inbox:
	handle(msg)
case out <- next:
	sent++
case <-quit:
	return
}

If several cases are ready at once, select picks one at random, so no channel can starve the others. If none is ready, it blocks.

Common patterns:

  • Timeouts: add case <-time.After(2 * time.Second): to give up waiting. In a long-running loop, prefer one time.NewTimer or a context deadline, because each time.After call creates a new timer.
  • Non-blocking operations: a default case runs immediately when nothing else is ready. select { case ch <- v: default: dropped++ } sends if possible and drops otherwise.
  • Cancellation: include case <-ctx.Done(): (next lessons) so a goroutine stops when its work is no longer wanted.
  • Event loops: for { select { ... } } is the standard shape for a goroutine that serves several channels.

A nil channel is never ready, which is a useful trick: set a channel variable to nil to disable its case once it's closed, while still serving the others.

An empty select {} blocks forever. And a default case inside a for loop turns it into a busy loop that burns CPU — only use default when you genuinely have something else to do.

Example

package main

import (
	"fmt"
	"time"
)

func slowQuery(d time.Duration) <-chan string {
	ch := make(chan string, 1) // buffered: sender never leaks
	go func() {
		time.Sleep(d)
		ch <- "result"
	}()
	return ch
}

func main() {
	select {
	case r := <-slowQuery(50 * time.Millisecond):
		fmt.Println(r)
	case <-time.After(10 * time.Millisecond):
		fmt.Println("timed out") // this one wins
	}

	events := make(chan int)
	select {
	case e := <-events:
		fmt.Println(e)
	default:
		fmt.Println("no event ready") // non-blocking
	}
}

Remember

  • select runs the first ready case; ties are broken randomly
  • time.After or a context gives you timeouts
  • default makes an operation non-blocking — avoid it in tight loops

Watch out for

  • Using an unbuffered result channel with a timeout: when the timeout wins, the sender blocks forever and leaks.

Check yourself

  1. In the example, why is slowQuery's channel buffered with capacity 1?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: go.dev/ref/spec#Select_statements

select and timeouts · Go · Topolearn