Select
Q7nl1s admin

什么是select?

select语句用于从多个发送/接收通道操作中进行选择。选择语句会阻塞,直到其中一个发送/接收操作准备好。如果多个操作都准备好了,就会随机选择其中一个。语法与switch类似,只是每个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
package main

import (
"fmt"
"time"
)

func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"

}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}

在上面的程序中,第8行中的server1函数休眠6秒,然后将文本从server1写到通道ch上。12行的server2函数休眠3秒,然后从server2写到通道ch(两个ch不同)。

主函数在第20行和第21行分别调用go Goroutine server1server2

在第22行,控制到达 select 语句。 select 语句阻塞,直到其中一个case准备就绪。在我们上面的程序中,server1的Goroutine在6秒后写到output1通道,而server2在3秒后写到output2通道。所以选择语句将阻塞3秒,并等待server2 Goroutine写到output2通道。3秒后,程序打印:

1
from server2  

然后就终止了。

select的实际使用

将上述程序中的函数命名为server1server2背后的原因是为了说明select的实际使用。

让我们假设我们有一个关键任务的应用程序,我们需要尽快将输出结果返回给用户。这个应用程序的数据库被复制并存储在世界各地的不同服务器上。假设函数server1server2实际上是与2个这样的服务器进行通信。每个服务器的响应时间取决于每个服务器的负载和网络延迟。我们向两个服务器发送请求,然后使用 select 语句在相应的通道上等待响应。首先响应的服务器被选择,其他的响应被忽略。这样,我们就可以向多个服务器发送相同的请求,并向用户返回最快的响应 :) 。

Default case(默认情况)

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
package main

import (
"fmt"
"time"
)

func process(ch chan string) {
time.Sleep(10500 * time.Millisecond)
ch <- "process successful"
}

func main() {
ch := make(chan string)
go process(ch)
for {
time.Sleep(1000 * time.Millisecond)
select {
case v := <-ch:
fmt.Println("received value: ", v)
return
default:
fmt.Println("no value received")
}
}

}

在上面的程序中,第8行中的process函数睡眠了10500毫秒(10.5秒),然后将process successful写入ch通道。这个函数在第15行被调用。

在同时调用process Goroutine之后,在main Goroutine中开始了一个无限的for循环。无限循环在每次迭代开始时都会休眠1000毫秒(1秒),然后执行选择操作。在最初的10500毫秒内,select语句的第一个case,即case v := <-ch:将不会被准备好,因为process Goroutine将在10500毫秒后才写到ch通道。因此,default case将在这段时间内被执行,程序将打印no value received10次。

10.5秒后,第10行process Goroutine将process successful写入ch通道。现在,select语句的第一种情况将被执行,程序将打印received value: process successful,然后它将终止。这个程序将输出:

1
2
3
4
5
6
7
8
9
10
11
no value received  
no value received
no value received
no value received
no value received
no value received
no value received
no value received
no value received
no value received
received value: process successful

死锁和默认情况

1
2
3
4
5
6
7
8
package main

func main() {
ch := make(chan string)
select {
case <-ch:
}
}

在上面的程序中,我们已经在第4行创建了一个通道ch。我们试图在第6行的select中读取这个通道。由于没有其他Goroutine写到这个通道,select语句将永远阻塞,因此将导致死锁。这个程序在运行时将出现报错,并有如下信息:

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
/tmp/sandbox627739431/prog.go:6 +0x4d

如果有一个default案例存在,这个死锁就不会发生,因为default案例将在没有其他case准备好的时候被执行。上面的程序是用下面的default案例重写的。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
ch := make(chan string)
select {
case <-ch:
default:
fmt.Println("default case executed")
}
}

上面的程序将被打印出来。

1
default case executed  

同样地,即使select只有nil通道,default情况也会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
var ch chan string
select {
case v := <-ch:
fmt.Println("received value", v)
default:
fmt.Println("default case executed")

}
}

在上面的程序中,chnil,我们试图从第8行的select中读取ch。如果没有default案例,select就会永远阻塞,造成死锁。由于我们在select里面有一个default案例,它将被执行,程序将被打印。

1
default case executed  

随机选择

当一个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
package main

import (
"fmt"
"time"
)

func server1(ch chan string) {
ch <- "from server1"
}
func server2(ch chan string) {
ch <- "from server2"

}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
time.Sleep(1 * time.Second)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}

在上面的程序中,server1server2的Goroutines分别在第18和19行被调用。然后,main程序在第20行休眠1秒。当控制到达第21行的select语句时,from server1已经从server1写到output1通道,from server2已经从server2写到output2通道,因此select语句的两种case都可以执行。如果你多次运行这个程序,输出将在from server1from server2之间变化,这取决于随机选择的情况。

请在你的本地系统中运行此程序以获得这种随机性。如果这个程序在playground上运行,它将打印相同的输出,因为playground是确定性的。

Gotcha-Empty select

1
2
3
4
5
package main

func main() {
select {}
}

你认为上面这个程序的输出会是什么?

我们知道select语句会阻塞,直到它的一个case被执行。在这种情况下,select语句没有任何case,因此它将永远阻塞,导致死锁。这个程序会出现以下的报错信息。

1
2
3
4
5
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:
main.main()
/tmp/sandbox246983342/prog.go:4 +0x25

祝你有个愉快的一天。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View