什么是 select?

select 语句用于从多个发送/接收 channel 操作中进行选择。 select 语句将阻塞,直到其中一个发送/接收操作准备就绪。如果准备好多个操作,则随机选择其中一个操作。语法类似于 switch,但是每个 case 语句都是一个 channel 操作。让我们深入研究一些代码,以便更好地理解。

Example

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func server1(ch chan string) {
  7. time.Sleep(6 * time.Second)
  8. ch <- "from server1"
  9. }
  10. func server2(ch chan string) {
  11. time.Sleep(3 * time.Second)
  12. ch <- "from server2"
  13. }
  14. func main() {
  15. output1 := make(chan string)
  16. output2 := make(chan string)
  17. go server1(output1)
  18. go server2(output2)
  19. select {
  20. case s1 := <-output1:
  21. fmt.Println(s1)
  22. case s2 := <-output2:
  23. fmt.Println(s2)
  24. }
  25. }

Run in playground

在上面的程序中,第 8 行 server1 函数休眠 6 秒然后将文本 from server1 写入 channel ch。第 13 行 server2 函数休眠 3 秒,然后 from server2 写入 channel ch

main 函数在第 21 行和第 22 行分别调用 Goroutine server1server2

在 23 行,程序到达 select 语句。select 语句将阻塞,直到其中一个 channel 就绪。在上面的程序中,server1 Goroutine 在 6 秒后写入 output1 channel ,而 server2 在 3 秒后写入 output2 channel 。因此 select 语句将阻塞 3 秒,并等待 server2 Goroutine 写入 output2 channel 。3 秒后,程序输出

  1. from server2

然后程序终止。

select 的实际用途

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

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

Default case

select 语句中的 default case 在其他情况都没有准备好时执行。这通常用于防止 select 语句阻塞。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func process(ch chan string) {
  7. time.Sleep(10500 * time.Millisecond)
  8. ch <- "process successful"
  9. }
  10. func main() {
  11. ch := make(chan string)
  12. go process(ch)
  13. for {
  14. time.Sleep(1000 * time.Millisecond)
  15. select {
  16. case v := <-ch:
  17. fmt.Println("received value: ", v)
  18. return
  19. default:
  20. fmt.Println("no value received")
  21. }
  22. }
  23. }

Run in playground

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

在同时调用 process Goroutine之后,在 main Goroutine 中启动了无限循环。无限循环在每次迭代开始期间休眠 1000 毫秒(1 秒),并执行选择操作。在前 10500 毫秒期间,select 语句的第一种情况即 case v := <-ch: 将不会准备就绪,因为Goroutine process 在10500毫秒后写入 ch channel 。因此,在此期间将执行 default 语句,程序将输出 10 次 no value received

在 10.5 秒之后,process Goroutine在第 10行 将 process successful写入 ch 。现在将执行 select 语句的第一种情况,程序将输出 received value: process successful,然后终止。该程序将输出,

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


死锁和 default case

  1. package main
  2. func main() {
  3. ch := make(chan string)
  4. select {
  5. case <-ch:
  6. }
  7. }

Run in playground

在上面的程序中,我们在第 4 行创建了一个 channel ch。我们尝试从第 6 行中选择的这个 channel 读取。 select 语句将永远阻塞,因为没有其他 Goroutine 写入此 channel ,因此将导致死锁。该程序将在运行时发生报错,

  1. fatal error: all goroutines are asleep - deadlock!
  2. goroutine 1 [chan receive]:
  3. main.main()
  4. /tmp/sandbox416567824/main.go:6 +0x80

如果存在 default case ,则不会发生此死锁,因为在没有其他情况准备就绪时将执行 default case 。上面的程序用下面的 default case 重写。

  1. package main
  2. import "fmt"
  3. func main() {
  4. ch := make(chan string)
  5. select {
  6. case <-ch:
  7. default:
  8. fmt.Println("default case executed")
  9. }
  10. }

Run in playground

以上程序将输出

  1. default case executed

类似地,即使 select 只有零 channel ,也会执行 default case 。

  1. package main
  2. import "fmt"
  3. func main() {
  4. var ch chan string
  5. select {
  6. case v := <-ch:
  7. fmt.Println("received value", v)
  8. default:
  9. fmt.Println("default case executed")
  10. }
  11. }

Run in playground

在上面的程序中,chnil,第 8 行我们试图从 ch 中的 select 中读取。如果default 情况不存在,则 select 将永远被阻塞并导致死锁。由于我们在 select 中有一个默认的情况,它将被执行并且程序将输出,

  1. default case executed


随机选择


select 语句中的多个 case 已准备就绪时,其中一个将随机执行。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func server1(ch chan string) {
  7. ch <- "from server1"
  8. }
  9. func server2(ch chan string) {
  10. ch <- "from server2"
  11. }
  12. func main() {
  13. output1 := make(chan string)
  14. output2 := make(chan string)
  15. go server1(output1)
  16. go server2(output2)
  17. time.Sleep(1 * time.Second)
  18. select {
  19. case s1 := <-output1:
  20. fmt.Println(s1)
  21. case s2 := <-output2:
  22. fmt.Println(s2)
  23. }
  24. }

Run in playground

在上面的程序中,server1server2 在 18 19 行并发调用。然后主程序在第 21 行中休眠 1 秒。 当程序到达第 22 行中的 select 语句时。server1 将从 from server1 写入 output1 channel ,server2from server2 写入 output2 channel ,因此 select 语句的两种情况都准备好执行。如果多次运行此程序,输出将在 from server1from server2 之间变化,具体取决于随机选择的情况。

请在本地系统中运行此程序以获得此随机性。如果该程序在 playground 上运行,它将输出相同的输出,因为 playground 结果是固定的。

疑难杂症 —— 空 Select

  1. package main
  2. func main() {
  3. select {}
  4. }

Run in playground

你认为上面程序的输出结果是什么?

我们知道 select 语句将被阻塞,直到执行其中一个案例。在这种情况下,select 语句没有任何情况,因此它将永远阻塞导致死锁。这个程序运行会得到报错,

  1. fatal error: all goroutines are asleep - deadlock!
  2. goroutine 1 [select (no cases)]:
  3. main.main()
  4. /tmp/sandbox299546399/main.go:4 +0x20


原文链接


https://golangbot.com/select/