原文来自:https://golangbot.com/select/
什么是select
select语句通常用来从多个读写通道中进行选择。select语句将会阻塞,直到有一个读写通道就绪。如果有多个读写通道就绪,会从中任意选择一个。select语法类似switch语句,除了每个case语句必须是通道操作。让我们深入代码以便更好的理解。
举例
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)
}
}
在上述程序中,server1函数先休眠6秒后往通道ch写入文本。server2函数休眠3秒后往通道ch写入文本。
在主函数中分别调用协程server1和server2.
代码流程走到select语句,select语句阻塞直到其中一个case语句就绪。在上述程序中,server1协程休眠6s后写入通道output1,而协程server2只休眠3秒就往通道output2写数据。所以select语句将会阻塞3秒等待协程server2往通道output2写数据。3秒后程序打印以下信息;
from server2
之后程序结束。
select应用
上述代码之所以命名函数为server1和server2,是为了解释实际使用select的场景。
让我们假设又一个关键业务应用,我们需要给用户尽可能快的返回信息。该应用的数据库是复杂的,而且存储在不通的世界各地不通的服务器上。假设函数server1和server2是实际上与这个两个服务器通信的。每个服务器的响应时间取决于每个服务器的负载和网络延迟。我们发送请求给这两个服务器,用select语句在相关通道上等待响应。第一个响应的服务器将被select语句所选择,其他响应将被忽略。同样的我们可以发送相同的请求给多个服务器,这样用户就可以得到最快的响应。
defalut case
select语句中的default 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")
}
}
}
在上述程序中,process函数休眠10.5秒后将“process successful”写入ch通道。该函数和主函数并发运行。
在并发调用process协程后,进入无限for循环,每次循环先休眠1秒,然后执行select语句。在前10.5秒,第一个case分支case v:= <-ch:通道未就绪,因为process协程休眠10.5秒。因此default case分支将被执行,程序打印“no value received"10次。
在10.5秒后,协程process写入”process sucessful“到通道ch中,这个时候第一个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
死锁和default case
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
在上述程序中,我们创建了一个通道ch。代码第6行,我们尝试从这个通道中读取数据。select语句将被永久阻塞,因为没有其他协程可以往这个通道中写入数据,因此造成死锁。程序将会在运行时奔溃,并提示:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox416567824/main.go:6 +0x80
如果有一个default case分支,死锁就不会发生。default case分支在没有其他case分支就绪的情况下将被执行。在下面增加default case重写上面程序:
package main
import "fmt"
func main() {
ch := make(chan string)
select {
case <-ch:
default:
fmt.Println("default case executed")
}
}
上述程序打印:
default case executed
同样的,default case分支在select语句仅拥有nil通道的情况下也可以被执行。
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")
}
}
随机选择
select语句在有多个case分支准备就绪的情况下,会随机选择一个分支执行。
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)
}
}
在上述代码中,server1和server2协程在第18,19行被调用。主程序先休眠1秒后到达select语句。这个时候server1和server2都已经往通道output1和output2写如数据,因此case分支都已经准备就绪,都可以执行。如果执行这个程序多次,结果将在”from server1“和”from server2“之间变化,变化却决于随机选择。
请在个人计算机上运行这个程序,才能获得随机的结果。playground上运行打印的结果时固定的。
Gotcha - 空select
package main
func main() {
select {}
}
你觉的上述程序的会出现什么结果?
我们直到select语句将会被阻塞直到其中一个case分支可执行。这个例子,select语句没有任何case分支,因此它将被永久阻塞导致死锁。程序将会奔溃提示:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
/tmp/sandbox299546399/main.go:4 +0x20
本节课结束。Have a good day!
ps:初次翻译,翻译的不好,请大家多多指教!