Skip to content

Intermediate · Concurrency: goroutines, channels and context · topic 28 of 46 · 30 min

select: waiting on several channels

Explanation

select waits on several channel operations and runs whichever is ready first; if several are ready it picks one at random. A case with time.After gives you a timeout, and a default case makes the select non-blocking. A for-select loop is the heart of many Go workers and servers.

Why select?

A goroutine often waits for more than one thing: a result OR a timeout, a new job OR a stop signal. Receiving from one channel would block and ignore the other. select is like a switch for channels: it waits until one of its cases can proceed, then runs that case.

go
select {
case msg := <-sms:
	fmt.Println("sms:", msg)
case mail := <-email:
	fmt.Println("email:", mail)
}
Blocks until either channel has a value. If both are ready at the same time, Go picks one at random, which keeps any channel from being starved.

Timeouts with time.After

time.After(d) returns a channel that receives a value once, after duration d. Put it in a select to give up when something takes too long.

go
result := make(chan string, 1)
go func() {
	time.Sleep(2 * time.Second) // slow bank
	result <- "payment confirmed"
}()

select {
case r := <-result:
	fmt.Println(r)
case <-time.After(500 * time.Millisecond):
	fmt.Println("timeout: show 'checking status...' to the user")
}
Prints the timeout line after 0.5 s. The result channel has a buffer of 1 so the slow goroutine can still send and finish, instead of blocking forever (a goroutine leak).
Careful: Goroutine leak: if nobody will ever receive from an unbuffered channel, the goroutine sending to it stays blocked forever and its memory is never freed. When you might stop listening (timeouts, cancellation), give the channel a buffer or make the sender also select on a done or ctx.Done() channel.

default: do not wait

go
select {
case job := <-jobs:
	process(job)
default:
	fmt.Println("no job right now")
}
With default, select never blocks: if no case is ready, default runs immediately.
go
select {
case events <- e:
	// sent
default:
	dropped++ // queue full: drop instead of slowing the caller
}
A non-blocking send: useful for metrics or logs where dropping is better than waiting.

The for-select loop

go
func worker(jobs <-chan int, quit <-chan struct{}) {
	for {
		select {
		case j := <-jobs:
			fmt.Println("job", j)
		case <-quit:
			fmt.Println("stopping")
			return
		}
	}
}
chan struct{} carries no data; closing it is a broadcast "stop" signal to every goroutine listening.

Closing a channel wakes up every receiver at once, because receiving from a closed channel succeeds immediately. That makes close(quit) a clean way to tell many goroutines to stop. In real code, ctx.Done() from the context package (later in this module) plays this role.

Fan-in: merging channels

go
func merge(a, b <-chan string) <-chan string {
	out := make(chan string)
	go func() {
		defer close(out)
		for a != nil || b != nil {
			select {
			case v, ok := <-a:
				if !ok {
					a = nil // a nil channel is never selected again
					continue
				}
				out <- v
			case v, ok := <-b:
				if !ok {
					b = nil
					continue
				}
				out <- v
			}
		}
	}()
	return out
}
Setting a finished channel to nil disables its case, because operations on a nil channel block forever.
Tip: For repeated work on a schedule, use time.NewTicker(d) and select on ticker.C; call ticker.Stop() when done. For a single delay, time.After is fine.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

select: waiting on several channels · 1/7

select

select lets a goroutine wait on several channels at once.

Prefer a video by a teacher? Find free videos on “golang select statement timeout time.After default case” on YouTube ↗

In real life: Fastest of two delivery-partner quotes

A courier aggregator asks two delivery partners for a price quote. It uses whichever answers first, and if neither answers within 800 ms it shows a standard rate, so the checkout page never hangs because a partner is slow.

go
package main

import (
	"fmt"
	"time"
)

func quote(name string, delay time.Duration, price int) <-chan string {
	ch := make(chan string, 1) // buffered: sender never gets stuck
	go func() {
		time.Sleep(delay)
		ch <- fmt.Sprintf("%s: Rs %d", name, price)
	}()
	return ch
}

func main() {
	a := quote("PartnerA", 300*time.Millisecond, 62)
	b := quote("PartnerB", 150*time.Millisecond, 70)
	select {
	case q := <-a:
		fmt.Println("using", q)
	case q := <-b:
		fmt.Println("using", q)
	case <-time.After(800 * time.Millisecond):
		fmt.Println("using standard rate Rs 80")
	}
}
Prints using PartnerB: Rs 70, because B answers first.

Practice (0/4 done)

Task 1 · Question

Two cases in a select are both ready. Which runs?

Task 2 · Question

What does this print? ch := make(chan int) select { case v := <-ch: fmt.Println(v) default: fmt.Println("empty") }

Task 3 · Question

Why give a result channel a buffer of 1 when the receiver might time out?

Task 4 · Practice

Write a function fetchWithTimeout(d time.Duration) (string, error) that starts a goroutine "fetching" a page (sleep 1 second, then send "page") and returns an error "timeout" if d passes first. Call it with 2 seconds and with 200 ms.

Quick quiz

Question 1

1.What does time.After(d) return?

Question 2

2.What does a default case do in select?

Question 3

3.Why set a channel variable to nil inside a fan-in loop?

Question 4

4.What is a goroutine leak?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does time.After(d) return?

Answer: A channel that receives once after d

It returns a <-chan time.Time that fires once.

What does a default case do in select?

Answer: Runs immediately if no other case is ready, so select does not block

default makes the select non-blocking.

Why set a channel variable to nil inside a fan-in loop?

Answer: Because operations on a nil channel block forever, so that case is disabled

A nil channel case can never be selected.

What is a goroutine leak?

Answer: A goroutine blocked forever that never ends, holding memory

Leaked goroutines pile up and waste memory over time.

What happens when you send on a closed channel?Related

Answer: panic: send on closed channel

Sending on a closed channel always panics.

From Channels: passing data between goroutines

What does errgroup.Group.Wait return?Related

Answer: The first non-nil error returned by any Go function

It waits for all functions and returns the first error.

From Concurrency interview questions and coding rounds

What happens to running goroutines when main returns?Related

Answer: They are stopped because the program exits

The program ends when main returns.

From Goroutines and WaitGroup

How do several threads send on the same channel?Related

Answer: Clone the Sender for each thread

Sender can be cloned; each clone can move into a thread.

From Message passing with channels

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Concurrency: goroutines, channels and context” before its exam: Mutex, Once, atomic and the race detector, context: timeouts and cancellation.
  2. Module 6 exam: Concurrency · 15 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Standard library essentials. 46 topics left in the course before the final exam.

← Previous: Channels: passing data between goroutines