select and Timeouts
A select statement waits on several channel operations at once and runs whichever is ready first. It's how a goroutine listens for results and a timeout and a "stop" signal at the same time. This lesson covers select, timeouts with time.After, non-blocking operations with default, tickers, and the for-select loop that long-running goroutines are built on.
Waiting on several channels
select looks like a switch, but each case is a send or receive. It blocks until one case can proceed, then runs that case only. If several are ready, it picks one at random:
package main import ( "fmt" "time" ) func main() { fast := make(chan string) slow := make(chan string) go func() { time.Sleep(200 * time.Millisecond) slow <- "slow server" }() go func() { time.Sleep(20 * time.Millisecond) fast <- "fast server" }() for range 2 { select { case msg := <-fast: fmt.Println("got", msg) case msg := <-slow: fmt.Println("got", msg) } } }
got fast server got slow server
Timeouts
time.After(d) returns a channel that receives a value after the duration d. Put it in a select to stop waiting when something takes too long:
package main import ( "fmt" "time" ) func query(delay time.Duration) <-chan string { ch := make(chan string, 1) // buffered: the sender never blocks, even if nobody receives go func() { time.Sleep(delay) ch <- "rows" }() return ch } func main() { for _, d := range []time.Duration{30 * time.Millisecond, 500 * time.Millisecond} { select { case res := <-query(d): fmt.Println("result:", res) case <-time.After(100 * time.Millisecond): fmt.Println("timed out after 100ms") } } }
result: rows timed out after 100ms
The result channel is buffered with room for one value. If the timeout wins, the slow goroutine can still send its result into the buffer and finish, instead of blocking forever — a leaked goroutine. For timeouts that must also stop the work itself, use a context (lesson 56).
Non-blocking operations with default
A default case runs immediately if no other case is ready, so the select never blocks. Use it to try a send or receive without waiting:
package main import "fmt" func main() { events := make(chan string, 2) for _, e := range []string{"click", "scroll", "resize"} { select { case events <- e: fmt.Println("queued", e) default: fmt.Println("queue full, dropped", e) } } select { case e := <-events: fmt.Println("handling", e) default: fmt.Println("nothing to do") } }
queued click queued scroll queue full, dropped resize handling click
The for-select loop
A goroutine that runs until told to stop is usually a for loop around a select, with one case for work and one for a "done" signal. Closing a channel is the standard way to broadcast "stop" — every receiver sees it at once:
package main import ( "fmt" "sync" ) func worker(jobs <-chan int, done <-chan struct{}, results chan<- string) { for { select { case j := <-jobs: results <- fmt.Sprintf("job %d done", j) case <-done: results <- "worker stopping" return } } } func main() { jobs := make(chan int) done := make(chan struct{}) results := make(chan string) var wg sync.WaitGroup wg.Go(func() { worker(jobs, done, results) }) for i := 1; i <= 3; i++ { jobs <- i fmt.Println(<-results) } close(done) fmt.Println(<-results) wg.Wait() }
job 1 done job 2 done job 3 done worker stopping
chan struct{} carries no data — it's used purely as a signal, and struct{} takes no memory.
Tickers
A time.Ticker delivers a value on its channel at a regular interval. Combine it with a stop condition in a for-select loop for periodic work such as polling or heartbeats, and always Stop it when finished:
package main import ( "fmt" "time" ) func main() { ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() deadline := time.After(180 * time.Millisecond) ticks := 0 for { select { case <-ticker.C: ticks++ fmt.Println("tick", ticks) case <-deadline: fmt.Println("finished after", ticks, "ticks") return } } }
tick 1 tick 2 tick 3 finished after 3 ticks
Disabling a case with a nil channel
A receive from a nil channel never proceeds, so setting a channel variable to nil switches its case off. This is the clean way to merge two channels until both are closed:
package main import "fmt" func send(vals ...int) <-chan int { ch := make(chan int) go func() { defer close(ch) for _, v := range vals { ch <- v } }() return ch } func main() { a, b := send(1, 2, 3), send(10, 20) sum := 0 for a != nil || b != nil { select { case v, ok := <-a: if !ok { a = nil // a is finished: stop selecting on it continue } sum += v case v, ok := <-b: if !ok { b = nil continue } sum += v } } fmt.Println("sum:", sum) }
sum: 36
Try it yourself
Ask three "mirrors" for the same file and use whichever answers first. Change the delays and see which one wins.
fastest mirror: asia