select
厨房同时做汤和米饭,各从自己的 channel 送出来。要是先 <-soup 再 <-rice,米饭早就好了也得等汤。select 能同时等几个 channel,哪个先好就先走哪个:
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("汤", 300*time.Millisecond, soup)17 go prepare("米饭", 100*time.Millisecond, rice)18 for range 2 {19 select {20 case s := <-soup:21 fmt.Println("端上", s)22 case r := <-rice:23 fmt.Println("端上", r)24 }25 }26}
端上 米饭 端上 汤
select 的样子像 switch,可每个 case 是一次 channel 操作。它先等着,直到某个 case 能走了,就走那一个,执行它下面的代码。prepare 的参数 d 是 time.Duration 类型,表示一段时长;300*time.Millisecond 就是一个 time.Duration。米饭 0.1 秒就好,所以先端上米饭;第二轮只剩汤。
点单步:走到 select,main 那条泳道变成「卡在 select」,两根虚线箭头同时指着 soup 和 rice。米饭送到时,泳道上出现「选了」,一根绿色箭头指着 rice。(网页上的 Sleep 不真的等:大家都在等时,时钟直接拨到下一个到点的时刻。)
default:都没好就不等
select 里可以写一个 default:所有 case 都走不了时,不等,马上执行 default。用它可以「看一眼有没有,没有就算了」:
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("收到", m)11 default:12 fmt.Println("没有新消息")13 }14 inbox <- "新订单"15 }16}
没有新消息 收到 新订单
第一轮 inbox 是空的,收不到,走了 default;第二轮里面有一条,就收到了。default 只在别的 case 都走不了时才走,不管写在第几个。
超时:time.After
time.After(时长) 给你一个 channel,过了这段时间,它会送来一个值。把它放进 select,就是「最多等这么久」:
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 + " 下好了"13 }()14 select {15 case msg := <-done:16 return msg17 case <-time.After(200 * time.Millisecond):18 return file + " 超时了"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 下好了 b.zip 超时了
a.txt 0.1 秒就下好,比 0.2 秒的限时早;b.zip 要 0.5 秒,0.2 秒一到 time.After 先送来值,就超时了。done 带 1 个缓冲:超时以后没人再收,下载的 goroutine 发送时也能把值放下就走,不会一直卡着。网页上的时钟是假的,不真的等;在你的电脑上会真的等。猜一猜:
这段程序显示什么?
1package main2 3import (4 "fmt"5 "time"6)7 8func main() {9 select {10 case <-time.After(300 * time.Millisecond):11 fmt.Println("300 毫秒")12 case <-time.After(100 * time.Millisecond):13 fmt.Println("100 毫秒")14 }15}
select 等到有一个 case 能走,就走那一个,而且只走一个。100 毫秒的先到,所以显示「100 毫秒」。case 写在前面还是后面没关系。
在 select 里发送
case 也可以是发送:channel 有空位、能发出去,就走这个 case。配上 default,就是「能放就放,放不下就算了」。订单队列只有 2 个空位:
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("收下", order)11 default:12 fmt.Println("排满了,没接", order)13 }14 }15 fmt.Println("队列里有", len(queue), "单")16}
收下 A01 收下 A02 排满了,没接 A03 排满了,没接 A04 队列里有 2 单
前两单放得进去;第三单时两个空位都满了,发送走不了,就走 default,不会卡住。
几个同时能走时
要是几个 case 同时都能走呢?真 Go 会随机挑一个,每次可能不一样:
1a := make(chan string, 1)2b := make(chan string, 1)3a <- "来自 a"4b <- "来自 b"5select {6case m := <-a:7 fmt.Println(m)8case m := <-b:9 fmt.Println(m)10}
在你的电脑上多跑几次,有时显示「来自 a」,有时「来自 b」。网页上的引擎总是挑最前面的那个,所以这里每次都一样,别依赖它。写程序时,不管挑中哪个,结果都要对。
for 加 select
select 常常放在 for 里,一直收,直到收到「完成了」的信号。关了的 channel 永远能收(收到零值),所以 close(finished) 正好当这个信号:
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("进度", p)20 case <-finished:21 fmt.Println("下载完成")22 return23 }24 }25}
进度 25 进度 50 进度 75 进度 100 下载完成
收到完成信号时用 return 结束 main。注意:在 select 里写 break,跳出的只是 select,不是外面的 for。
要是 download 忘了 close(finished),select 两个 case 都再也等不到:
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("进度", p)19 case <-finished:20 fmt.Println("下载完成")21 return22 }23 }24}
进度 50
进度 100
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select]:
main.main(...)
/tmp/sandbox/prog.go:16 +0x1d
exit status 2发生了什么: 死锁:所有 goroutine 都在等,谁也动不了(比如往没人收的 channel 发,或者 Wait 等不到 Done)。
又是死锁,这次 goroutine 1 [select]: 说 main 卡在 select。点单步走到最后,看 main 的两根虚线箭头指着两个再也不会有值的 channel,泳道变红。一个 case 都没有的 select {} 也会永远等下去。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
下载超时就放弃
✓写好 fetch 里的 select:done 先送来结果就返回它;先过了 0.2 秒,就返回 文件名 超时了。程序会显示两个文件的结果。
photo.jpg 下好了 movie.mp4 超时了
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 + " 下好了"13 }()14 // 在这里写 select15 return ""16}17 18func main() {19 fmt.Println(fetch("photo.jpg", 50*time.Millisecond))20 fmt.Println(fetch("movie.mp4", 900*time.Millisecond))21}
队列满了就不接
✓写好 accept:把订单一个个用 select 往有 room 个空位的队列里放,放得进去就算收下,放不进去就算没接,最后返回收下和没接的单数。
队列里 2 单 收下 2 单,没接 3 单 队列里 3 单 收下 3 单,没接 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 // 用 select 往 queue 里放10 }11 fmt.Println("队列里", len(queue), "单")12 return taken, refused13}14 15func main() {16 t, r := accept([]string{"A1", "A2", "A3", "A4", "A5"}, 2)17 fmt.Printf("收下 %d 单,没接 %d 单\n", t, r)18 t, r = accept([]string{"B1", "B2", "B3"}, 4)19 fmt.Printf("收下 %d 单,没接 %d 单\n", t, r)20}
修好它:只收到一个
✓两个 goroutine 分别算两张订单的合计,订单 B 算得快。程序只显示了一张。改好它,让两张都显示,谁先算好谁在前面:B 合计 100,然后 A 合计 50。
B 合计 100 A 合计 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 合计", s)25 case s := <-b:26 fmt.Println("B 合计", s)27 }28}
这一课学会了
select同时等几个 channel 操作,哪个case先能走就走哪个,而且只走一个。default:所有case都走不了时马上执行,不等。case <-time.After(d):做超时;网页上的时钟是假的,不真的等。case ch <- v:也能放进select;配上default就是「放不下就算了」。- 几个
case同时能走时,真 Go 随机挑,程序不能依赖挑中哪个。 for加select一直收,close(finished)当完成信号;select里的break只跳出select。- 单步时「卡在 select」的泳道有几根虚线箭头,「选了」那根是绿色的。