原文地址:https://golangbot.com/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函数sleep 6秒之后往ch写数据,server2函数sleep 3秒后往ch写数据。main函数中创建了server1和server2两个协程,然后select模式将阻塞主协程直到其中一个case满足,在上面的例子中server1会休眠6秒而server2会休眠3秒,所以3秒后select模式会收到server2中ch通道的写入事件,打印出:
from server2
然后程序运行结束。
select的实际应用
让我们假设有一个关键性的应用,需要高响应回复用户,这个应用的数据库是分别存放在世界各地不同的服务器上,就好像我们是server1和serve2一样,服务器相应的时间是根据各自的网络延时出现不同的时延,我们发送请求给各个服务器,然后通过使用select模式就可以帮我们把最先相应的服务器筛选出来,而忽略其他响应速度慢的服务器。这样我们就可以尽快的把结果返回给用户了:)
Default 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")
}
}
}
在上面的例子中,process函数会sleep 10.5秒后往ch中写入数据。main中启动process协程后,定义了一个循环,每次sleep 1秒,在之后的10.5秒钟select会先查看case v:= <-ch:,这时是不会就绪的,所以转到default执行print,直到10.5秒之后ch通道中写入了数据case v才会执行,然后输出结果跳出循环。结果打印如下:
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
Deadlock and default case
package main
func main() {
ch := make(chan string)
select {
case <-ch:
}
}
上面例子中我们创建了一个通道ch,我们尝试从中读取数据但是这种情况永远不会发生,这时就会出现死锁现象。程序会打印报错:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox416567824/main.go:6 +0x80
如果我们加上default 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通道的话,default 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")
}
}
随机选择
在一个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)
}
}
在playground运行
在上面的代码中,函数server1和server2都会往通道中写入数据,在 main函数中当启动两个协程后,sleep 1秒,这时select会接收到case s1和case s2都是就绪状态,会随机从中选择一个进行执行。具体哪个执行完全是取决随机数的。
Gotcha - 空的select
package main
func main() {
select {}
}
你认为上面会出现什么情况?我们知道select会阻塞住直到有其中一个case得到满足。上面的情况没有一个case所以会出现死锁的情况,打印如下:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
/tmp/sandbox299546399/main.go:4 +0x20
这节就到这里,愉快一天!