Go语言 select语句


导言

  • 原文链接: Part 24: Select
  • If translation is not allowed, please leave me in the comment area and I will delete it as soon as possible.

Select

select 是什么?

select语句 用于从多个通道中选择操作,并执行。select语句 会有如下的可能:

  1. select 会处于阻塞状态,直到 发送/接收 操作之一准备就绪。
  2. 如果有一个操作准备就绪,select 会马上执行它。
  3. 如果有多个操作准备就绪,select 会随机的挑选出一个,并执行。

select 的句式与 switch 是类似的,区别在于:select 的每个 case 都只能是通道操作。
接下来,通过下面的代码,我们来更好的理解 select


实例

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 秒后,将 from server1 写入通道。而在第 12 行,server2函数休眠 3 秒后,也将 from server2 写入通道。

在第 2021行,main函数 分别调用两个协程:server1server2

在第 22 行,控件到达 select 语句。select 会阻塞,直到它的case 之一准备就绪。在上面的程序中,6 秒后,server1 会把 "from server1 写入 通道output1 中,而 server23 秒时,就已经把 from server2 写入 通道output2 中了。所以,通道只会阻塞 3 秒 — 等待 server2协程 将信息写入 output2通道 中。最终输出如下:

from server2

随后,程序结束。


select 的实际使用

在上面的代码中,之所以把函数命名为 server1server2,是为了说明 select 的实际用法。

假设我们有一个应用程序,它需要将输出尽快地展示给用户。这个程序的数据库分布在世界上的不同地点,数据库内容都是一样的。假如,server1server2 正在与 2 个数据库服务器通信。每个服务器的响应时间,取决于当前的负载及网络延迟。我们向这 2 个服务器发送请求后,使用 select语句,在相应的通道上等待响应。首次响应将会被 select 所捕获,之后的响应都会被忽略。通过这种方式,我们就可以给多个服务器,发送相同的请求,并将最快的响应呈现给用户。


默认 case

当没有 case 准备就绪时,select 会执行默认的 case。默认case 能避免 select 阻塞。

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 毫秒后,将 process successful 写入 通道ch 中。
15 行,我们调用了这个函数。在调用 process协程 后,我们 在 main协程 中开启了一个死循环。这个循环会先休眠 1000 毫秒,之后执行 select 操作,循环往复。
10500 毫秒前,case v:= <-ch 并没有准备好,因为此时 process协程 处于休眠状态。因此,在 process协程 休眠的这段时间,默认case 会被执行 10 次,程序会输出 10no value received

10.5 秒后,process协程 休眠结束,并将 process successful 写入 通道ch。此时,select语句 的第一个 case 准备就绪,于是,程序会打印 received value: process successful。随后,程序终止。

程序的所有输出如下:

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

死锁 与 默认case

package main

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

在上面程序的第 4 行,我们创建了一个 通道ch。在第 6 行,select 试图从这个通道读取数据。此时,select 将会永远阻塞,因为没有其他协程向 通道ch 写入数据。于是,死锁出现了。

运行这个程序,会出现如下的异常信息:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:  
main.main()  
    /tmp/sandbox416567824/main.go:6 +0x80

如果存在 默认case,此时就不会出现死锁,因为当没有 case 准备就绪时,默认case 会被执行。

我们使用 默认case,重写上面的代码。

package main

import "fmt"

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

输出如下:

default case executed  

类似地,即使 select 中只有 nil通道,默认case 也会被执行。

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")

    }
}

在上面程序的第 8行,通道chnil,我们试着在它内部读取数据。如果 默认case 不存在,select 将会一直阻塞,产生死锁。由于在 select 中有 默认case,默认case 将被执行。

程序输出如下:

default case executed 

随机选择

select 内部有多个 case 准备就绪,select 只会随机的挑选其中一个执行。

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)
    }
}

在上面程序的第 1819 行,我们分别开启了 server1 server2 协程。在第 20 行,main函数 休眠了 1 秒。当控件到达第 21 行时,server1server2 都已将数据写入对应的通道,即此时 select 中有多个 case 准备就绪。
运行上面的程序,你会发现:每次的输出不是固定的,输出在 from server1from server2 之中徘徊。

为了观察随机性,请在你的本机上运行上面的程序。


疑难杂症 — 空 select

package main

func main() {  
    select {}
}

猜猜上面程序的输出。

我们已经知道:select 会阻塞,直到它的任意一个 case 被执行。而在上面的情况中, select 没有任何的 case,因此,select 将会永远阻塞,从而导致死锁。

运行这个程序,程序会有如下异常:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:  
main.main()  
    /tmp/sandbox299546399/main.go:4 +0x20

全剧终!
祝你暴富~


原作者留言

优质内容来之不易,您可以通过该 链接 为我捐赠。


最后

感谢原作者的优质内容。

这是我的第八次翻译,欢迎指出文中的任何错误。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值