select 语句用于在多个发送/接收信道操作中进行选择。​​select​​​ 语句会一直阻塞,直到发送/接收操作准备就绪。如果有多个信道操作准备完毕, ​​select​​​ 会随机地选取其中之一执行。​​select-case​​​ 跟 ​​switch-case​​​ 相比,用法比较单一,它仅能用于 信道/通道 的相关操作。​​select-case​​ 模型如下:

select {
case expression1:
code
case expression2:
code
default:
code
}

下面是使用 ​​select-case​​ 的一个简单例子:

package main

import "fmt"

func main() {
// 创建两个信道
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
// 往信道 1 发送数据 100
ch1 <- 100
// 往信道 2 发送数据 200
ch2 <- 200

select {
// 如果从信道 1 收到数据
case message1 := <-ch1:
fmt.Println("ch1 received:", message1)
// 如果从信道 2 收到数据
case message2 := <-ch2:
fmt.Println("ch2 received:", message2)
// 默认输出
default:
fmt.Println("No data received.")
}
}

上面的程序创建了两个信道,并在执行 ​​select​​​ 语句之前往信道 1 和信道 2 分别发送数据,在执行 ​​select​​​ 语句时,如果有机会的话会运行所有表达式,只要其中一个信道接收到数据,那么就会执行对应的 ​​case​​ 代码,然后退出。所以运行该程序可能输出下面的语句:

ch1 received: 100

也有可能输出下面的这条语句,具体看哪个信道首先接收到数据:

ch2 received: 200

Go 语言系列27:Select_数据

select 的应用

Go 语言系列27:Select_死锁_02


利用 ​​select​​ 的特性,可以将其使用在获取服务器响应上。

假设某个应用的数据库复制并且存储在世界各地的服务器上。假设函数 ​​server1​​​ 和 ​​server2​​​ 与这样不同区域的两台服务器进行通信。每台服务器的负载和网络时延决定了它的响应时间。我们向两台服务器发送请求,并使用 ​​select​​​ 语句等待相应的信道发出响应。​​select​​ 会选择首先响应的服务器,而忽略其它的响应。使用这种方法,我们可以向多个服务器发送请求,并给用户返回最快的响应。

下面的程序模拟了这种服务:

package main

import (
"fmt"
"time"
)

func server1(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "server 1"
}

func server2(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "server 2"
}

func main() {
// 创建两个信道
ch1 := make(chan string)
ch2 := make(chan string)

go server1(ch1)
go server2(ch2)

select {
// 如果从信道 1 收到数据
case message1 := <-ch1:
fmt.Println("ch1 received:", message1)
// 如果从信道 2 收到数据
case message2 := <-ch2:
fmt.Println("ch2 received:", message2)
}
}

运行该程序输出如下:

ch1 received: server 1

当然,比较上面两个程序会发现,下面这个例子中没有 ​​default​​​ 分支,因为如果加了该默认分支,如果还没从信道接收到数据, ​​select​​​ 语句就会直接执行 ​​default​​ 分支然后退出,而不是被阻塞。


Go 语言系列27:Select_数据

造成死锁

Go 语言系列27:Select_死锁_02


上面的例子引出了一个新的问题,那就是如果没有 ​​default​​​ 分支, ​​select​​​ 就会阻塞,如果一直没有命中其中的某个 ​​case​​ 最后会造成死锁。

package main

import (
"fmt"
)

func main() {
// 创建两个信道
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)

select {
// 如果从信道 1 收到数据
case message1 := <-ch1:
fmt.Println("ch1 received:", message1)
// 如果从信道 2 收到数据
case message2 := <-ch2:
fmt.Println("ch2 received:", message2)
}
}

运行上面的程序会造成死锁。解决该问题的方法是写好 ​​default​​ 分支。

当然还有另一种情况会导致死锁的发生,那就是使用空 ​​select​​ :

package main

func main() {
select {}
}

运行上面的程序会抛出 ​​panic​​ 。


Go 语言系列27:Select_数据

随机性

Go 语言系列27:Select_死锁_02


前面学习 ​​switch-case​​​ 的时候,里面的 ​​case​​​ 是顺序执行的,但在 ​​select​​​ 里并不是顺序执行的。在上面的第一个例子就可以看出,当 ​​select​​​ 由多个 ​​case​​ 准备就绪时,将会随机地选取其中之一去执行。


Go 语言系列27:Select_数据

select 的超时

Go 语言系列27:Select_死锁_02


当 ​​case​​​ 里的信道始终没有接收到数据时,而且也没有 ​​default​​​ 语句时, ​​select​​​ 整体就会阻塞,但是有时我们并不希望 ​​select​​ 一直阻塞下去,这时候就可以手动设置一个超时时间。

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)
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 <-timeout:
fmt.Println("Timeout, exit.")
}
}

运行上面的程序输出如下:

Timeout, exit.

Go 语言系列27:Select_数据

读取/写入数据

Go 语言系列27:Select_死锁_02


​select​​​ 里的 ​​case​​ 表达式只能对信道进行操作,不管你是往信道写入数据,还是从信道读出数据。

package main

import (
"fmt"
)

func main() {
c1 := make(chan int, 2)

c1 <- 2
select {
case c1 <- 4:
fmt.Println("c1 received: ", <-c1)
fmt.Println("c1 received: ", <-c1)
default:
fmt.Println("channel blocking")
}
}

运行上面的程序输出如下:

c1 received:  2
c1 received: 4

参考文献:

[1] Alan A. A. Donovan; Brian W. Kernighan, Go 程序设计语言, Translated by 李道兵, 高博, 庞向才, 金鑫鑫 and 林齐斌, 机械工业出版社, 2017.