第 16 章 Select
select 语句用在多个发送/接收通道操作中进行选择。
select
语句会一直阻塞,直到发送/接收操作准备就绪。
- 如果有多个通道操作准备完毕,
select
会随机地选取其中之一执行。
select
语法如下:
1 2 3 4 5 6 7 8
| select { case expression1: code case expression2: code default: code }
|
下面是使用 select-case
的一个简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package main
import "fmt"
func main() { ch1 := make(chan string, 1) ch2 := make(chan string, 1) ch3 := make(chan string, 1) ch1 <- "Go语言微服务架构核心22讲" ch2 <- "从0到Go语言微服务架构师" ch3 <- "Go语言极简一本通"
select { case message1 := <-ch1: fmt.Println("ch1 received:", message1) case message2 := <-ch2: fmt.Println("ch2 received:", message2) case message3 := <-ch3: fmt.Println("ch3 received:", message3) default: fmt.Println("No data received.") } }
|
上面的程序创建了 3 个通道,并在执行 select
语句之前往通道 1 、通道 2 和 通道 3 分别发送数据,在执行 select
语句时,如果有机会的话会运行所有表达式,只要其中一个通道接收到数据,那么就会执行对应的 case
代码,然后退出。所以运行该程序可能输出下面的语句:
1
| ch1 received: Go语言微服务架构核心22讲
|
也有可能输出下面的这条语句,具体看哪个通道首先接收到数据:
1 2
| ch2 received: 从0到Go语言微服务架构师 ch3 received: Go语言极简一本通
|
16.1 select 的应用
每个任务执行的时间不同,使用 select
语句等待相应的通道发出响应。select
会选择首先响应先完成的 task,而忽略其它的响应。使用这种方法,我们可以做多个 task,并给用户返回最快的 task 结果。
下面的程序模拟了这种服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| package main
import ( "fmt" "time" )
func task1(ch chan string) { time.Sleep(5 * time.Second) ch <- "Go语言极简一本通" }
func task2(ch chan string) { time.Sleep(7 * time.Second) ch <- "Go语言微服务架构核心22讲" }
func task3(ch chan string) { time.Sleep(2 * time.Second) ch <- "从0到Go语言微服务架构师" }
func main() { ch1 := make(chan string) ch2 := make(chan string) ch3 := make(chan string) go task1(ch1) go task2(ch2) go task3(ch3)
select { case message1 := <-ch1: fmt.Println("ch1 received:", message1) case message2 := <-ch2: fmt.Println("ch2 received:", message2) case message3 := <-ch3: fmt.Println("ch3 received:", message3) } }
|
当然,上面的程序会发现,没有 default
分支,因为如果加了该默认分支,如果还没从通道接收到数据, select
语句就会直接执行 default
分支然后退出,而不是被阻塞。
16.2 造成死锁
上面的例子引出了一个新的问题,那就是如果没有 default
分支, select
就会阻塞,如果一直没有命中其中的某个 case
最后会造成死锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package main
import ( "fmt" )
func main() { ch1 := make(chan string, 1) ch2 := make(chan string, 1) ch3 := make(chan string, 1)
select { case message1 := <-ch1: fmt.Println("ch1 received:", message1) case message2 := <-ch2: fmt.Println("ch2 received:", message2) case message3 := <-ch3: fmt.Println("ch3 received:", message3) } }
|
运行上面的程序会造成死锁。解决该问题的方法是写好 default
分支。
当然还有另一种情况会导致死锁的发生,那就是使用空 select
:
1 2 3 4 5
| package main
func main() { select {} }
|
运行上面的程序会抛出 panic
。
Tips:
- 前面学习
switch-case
的时候,里面的 case
是顺序执行的,但在 select
里并不是顺序执行的。在上面的第一个例子就可以看出,当 select
由多个 case
准备就绪时,将会随机地选取其中之一去执行。
16.3 select 超时处理
当 case
里的通道始终没有接收到数据时,而且也没有 default
语句时, select
整体就会阻塞,但是有时我们并不希望 select
一直阻塞下去,这时候就可以手动设置一个超时时间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package main
import ( "fmt" "time" )
func makeTimeout(ch chan bool, t int) { time.Sleep(time.Second * time.Duration(t)) ch <- true }
func main() { c1 := make(chan string, 1) c2 := make(chan string, 1) c3 := make(chan string, 1) timeout := make(chan bool, 1)
go makeTimeout(timeout, 2)
select { case msg1 := <-c1: fmt.Println("c1 received: ", msg1) case msg2 := <-c2: fmt.Println("c2 received: ", msg2) case msg3 := <-c3: fmt.Println("c3 received: ", msg3) case <-timeout: fmt.Println("Timeout, exit.") } }
|
16.4 读取/写入数据
select
里的 case
表达式只能对通道进行操作,不管你是往通道写入数据,还是从通道读出数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package main
import ( "fmt" )
func main() { c1 := make(chan string, 2)
c1 <- "从0到Go语言微服务架构师" select { case c1 <- "Go语言微服务架构核心22讲": fmt.Println("c1 received: ", <-c1) fmt.Println("c1 received: ", <-c1) default: fmt.Println("channel blocking") } }
|
如何学习Go语言微服务,快速步入架构师
添加微信 |
公众号更多内容 |
 |
 |