select
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:
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}
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":
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}
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":
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}
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:
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}
select waits until one case can go, then takes that one, and only that one. The 100 ms timer fires first, so it shows "100 ms". Whether a case is written first or last doesn't matter.
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:
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}
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:
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}
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:
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}
Progress 50
Progress 100
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select]:
main.main(...)
/tmp/sandbox/prog.go:16 +0x1d
exit status 2What 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.
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.
photo.jpg downloaded movie.mp4 timed out
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}
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.
In the queue: 2 Accepted 2, turned away 3 In the queue: 3 Accepted 3, turned away 0
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}
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.
B total 100 A total 50
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}
What you learned
selectwaits on several channel operations and takes whichevercasecan go first, and only that one.defaultruns right away when nocasecan go, so theselectdoesn'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 aselecttoo; withdefaultit 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. forplusselectkeeps receiving, withclose(finished)as the done signal; abreakin aselectonly leaves theselect.- Step shows a lane "waiting in select" with several dashed arrows; the one it took turns green.