Kodewick
Start learning →
Lesson 21 of 24Contents

select

Lesson 21 of 24 · Part 5 · Concurrency · about 15 minutes

A kitchen makes soup and rice at the same time, each sent out on its own channel. If you do <-soup and then <-rice, the rice waits for the soup even when it's been ready for ages. select waits on several channels at once and goes with whichever is ready first:

prog.go
1package main2 3import (4	"fmt"5	"time"6)7 8func prepare(dish string, d time.Duration, out chan string) {9	time.Sleep(d)10	out <- dish11}12 13func main() {14	soup := make(chan string)15	rice := make(chan string)16	go prepare("soup", 300*time.Millisecond, soup)17	go prepare("rice", 100*time.Millisecond, rice)18	for range 2 {19		select {20		case s := <-soup:21			fmt.Println("Serving", s)22		case r := <-rice:23			fmt.Println("Serving", r)24		}25	}26}
Output
Serving rice
Serving soup

select looks like a switch, but each case is a channel operation. It waits until one of the cases can go, then goes with that one and runs the code under it. The parameter d of prepare has type time.Duration, a type for a length of time; 300*time.Millisecond is one. The rice is ready after 0.1 seconds, so it's served first; in the second round only the soup is left.

Press Step: at select the main lane turns to "waiting in select", with two dashed arrows pointing at both soup and rice. When the rice arrives, the lane shows "took" and a green arrow points at rice. (Sleep on this page doesn't really wait: when everyone is waiting, the clock jumps to the next wake-up time.)

default: don't wait if nothing is ready

A select can have a default: when no case can go, it doesn't wait but runs default right away. Use it to "take a look, and move on if there's nothing":

prog.go
1package main2 3import "fmt"4 5func main() {6	inbox := make(chan string, 1)7	for range 2 {8		select {9		case m := <-inbox:10			fmt.Println("Got", m)11		default:12			fmt.Println("No new messages")13		}14		inbox <- "a new order"15	}16}
Output
No new messages
Got a new order

In the first round inbox is empty, nothing can be received, and default runs. In the second round there's a message, so it's received. default only runs when no other case can go, wherever you write it.

Timeouts: time.After

time.After(duration) gives you a channel that delivers a value once that much time has passed. Put it in a select and you get "wait at most this long":

prog.go
1package main2 3import (4	"fmt"5	"time"6)7 8func download(file string, d time.Duration) string {9	done := make(chan string, 1)10	go func() {11		time.Sleep(d)12		done <- file + " downloaded"13	}()14	select {15	case msg := <-done:16		return msg17	case <-time.After(200 * time.Millisecond):18		return file + " timed out"19	}20}21 22func main() {23	fmt.Println(download("a.txt", 100*time.Millisecond))24	fmt.Println(download("b.zip", 500*time.Millisecond))25}
Output
a.txt downloaded
b.zip timed out

a.txt is done in 0.1 seconds, before the 0.2-second limit. b.zip needs 0.5 seconds, so at 0.2 time.After delivers first and it times out. done has a buffer of 1: after a timeout nobody receives from it, and the buffer lets the download goroutine drop its value off and finish instead of being stuck forever. The clock on this page is fake and doesn't really wait; on your computer it does. Guess:

Guess first

What does this show?

1package main2 3import (4	"fmt"5	"time"6)7 8func main() {9	select {10	case <-time.After(300 * time.Millisecond):11		fmt.Println("300 ms")12	case <-time.After(100 * time.Millisecond):13		fmt.Println("100 ms")14	}15}

Sending in select

A case can also be a send: if the channel has room and the value can go out, that case runs. Add a default and you get "put it in if there's room, otherwise skip it". The order queue has only 2 slots:

prog.go
1package main2 3import "fmt"4 5func main() {6	queue := make(chan string, 2)7	for _, order := range []string{"A01", "A02", "A03", "A04"} {8		select {9		case queue <- order:10			fmt.Println("Accepted", order)11		default:12			fmt.Println("Queue full, turned away", order)13		}14	}15	fmt.Println("Orders in the queue:", len(queue))16}
Output
Accepted A01
Accepted A02
Queue full, turned away A03
Queue full, turned away A04
Orders in the queue: 2

The first two orders fit. By the third, both slots are full, so the send can't go and default runs instead of getting stuck.

When several are ready

What if several cases can go at the same moment? Real Go picks one at random, and it can differ from run to run:

1a := make(chan string, 1)2b := make(chan string, 1)3a <- "from a"4b <- "from b"5select {6case m := <-a:7	fmt.Println(m)8case m := <-b:9	fmt.Println(m)10}

Run that on your computer a few times and you'll see "from a" sometimes and "from b" other times. The engine on this page always picks the first one, so here it's the same every time; don't rely on that. Write programs that come out right whichever case is picked.

for with select

select often sits inside a for, receiving over and over until a "done" signal arrives. A closed channel can always be received from (you get the zero value), so close(finished) makes a good signal:

prog.go
1package main2 3import "fmt"4 5func download(progress chan int, finished chan bool) {6	for p := 25; p <= 100; p += 25 {7		progress <- p8	}9	close(finished)10}11 12func main() {13	progress := make(chan int)14	finished := make(chan bool)15	go download(progress, finished)16	for {17		select {18		case p := <-progress:19			fmt.Println("Progress", p)20		case <-finished:21			fmt.Println("Download complete")22			return23		}24	}25}
Output
Progress 25
Progress 50
Progress 75
Progress 100
Download complete

When the done signal arrives, return ends main. Watch out: a break inside a select only leaves the select, not the for around it.

If download forgets close(finished), neither case can ever go:

prog.go
1package main2 3import "fmt"4 5func download(progress chan int, finished chan bool) {6	for p := 50; p <= 100; p += 50 {7		progress <- p8	}9}10 11func main() {12	progress := make(chan int)13	finished := make(chan bool)14	go download(progress, finished)15	for {16		select {17		case p := <-progress:18			fmt.Println("Progress", p)19		case <-finished:20			fmt.Println("Download complete")21			return22		}23	}24}
Output
Progress 50
Progress 100
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select]:
main.main(...)
	/tmp/sandbox/prog.go:16 +0x1d
exit status 2

What happened: Every goroutine is waiting (on a channel, a lock or a WaitGroup) and none can go on: a deadlock.

A deadlock again, and this time goroutine 1 [select]: says main is stuck in select. Press Step through to the end and see main's two dashed arrows pointing at two channels that will never get a value, and the lane turn red. A select {} with no cases at all waits forever too.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Give up on a slow download

Write the select in fetch: if done delivers first, return its result; if 0.2 seconds pass first, return filename timed out. The program shows the results for two files.

It should print
photo.jpg downloaded
movie.mp4 timed out
exercise1.go
1package main2 3import (4	"fmt"5	"time"6)7 8func fetch(file string, d time.Duration) string {9	done := make(chan string, 1)10	go func() {11		time.Sleep(d)12		done <- file + " downloaded"13	}()14	// write the select here15	return ""16}17 18func main() {19	fmt.Println(fetch("photo.jpg", 50*time.Millisecond))20	fmt.Println(fetch("movie.mp4", 900*time.Millisecond))21}
Output
Press Run or Ctrl+Enter
Exercise 2

Turn orders away when the queue is full

Finish accept: use select to put the orders one by one into a queue with room slots. One that fits is accepted, one that doesn't is turned away; return how many of each.

It should print
In the queue: 2
Accepted 2, turned away 3
In the queue: 3
Accepted 3, turned away 0
exercise2.go
1package main2 3import "fmt"4 5func accept(orders []string, room int) (int, int) {6	queue := make(chan string, room)7	taken, refused := 0, 08	for range orders {9		// put each order into queue with a select10	}11	fmt.Println("In the queue:", len(queue))12	return taken, refused13}14 15func main() {16	t, r := accept([]string{"A1", "A2", "A3", "A4", "A5"}, 2)17	fmt.Printf("Accepted %d, turned away %d\n", t, r)18	t, r = accept([]string{"B1", "B2", "B3"}, 4)19	fmt.Printf("Accepted %d, turned away %d\n", t, r)20}
Output
Press Run or Ctrl+Enter
Exercise 3

Fix it: only one arrives

Two goroutines add up two orders, and order B is quicker. The program shows only one of them. Fix it so both show, whichever is ready first on top: B total 100, then A total 50.

It should print
B total 100
A total 50
exercise3.go
1package main2 3import (4	"fmt"5	"time"6)7 8func total(prices []int, d time.Duration, out chan int) {9	time.Sleep(d)10	sum := 011	for _, p := range prices {12		sum += p13	}14	out <- sum15}16 17func main() {18	a := make(chan int)19	b := make(chan int)20	go total([]int{12, 30, 8}, 300*time.Millisecond, a)21	go total([]int{99, 1}, 100*time.Millisecond, b)22	select {23	case s := <-a:24		fmt.Println("A total", s)25	case s := <-b:26		fmt.Println("B total", s)27	}28}
Output
Press Run or Ctrl+Enter

What you learned

  • select waits on several channel operations and takes whichever case can go first, and only that one.
  • default runs right away when no case can go, so the select doesn't wait.
  • case <-time.After(d): makes a timeout; the clock on this page is fake and doesn't really wait.
  • A send like case ch <- v: can go in a select too; with default it means "skip it if there's no room".
  • When several cases are ready at once, real Go picks one at random, so don't depend on which.
  • for plus select keeps receiving, with close(finished) as the done signal; a break in a select only leaves the select.
  • Step shows a lane "waiting in select" with several dashed arrows; the one it took turns green.
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.