select 的实现在目前 go 的 master 分支已经有所修改,所以本文只针对 1.10 之前的版本。
基本用法
非阻塞 select:
select {
case <-ch:
default:
}
当 ch 中无数据时,不会阻塞,会直接走 default 分支。这种特性可以用来实现 trylock。
对 nil 进行发送或者接收都会永远阻塞,被包在 select 里也不例外:
var a chan int
select {
case <-ch:
}
println("can not be printed")
for select 组合起来用很常见,不过需要注意,下面的两种场景可能会造成问题:
for {
select {
case d := <-ch:
default:
}
}
这种写法比较危险,如果 ch 中没有数据就会一直死循环 cpu 爆炸。
for {
select {
case d := <-ch:
}
}
你以为这么写就没事了?啊当然,一般情况下还好。但如果 ch 被其它 goroutine close 掉了,那么 d:= <-ch 这种形式就是永远不阻塞,并且会一直返回零值了。如果不想关注这种情况,并且 select 中实际只操作一个 channel,建议写成 for range 形式:
for d := range ch {
// do some happy things with d
}
这样 ch 关闭的时候 for 循环会自动退出。
如果 select 中需要监听多个 channel,并且这些 channel 可能被关闭,那需要老老实实地写双返回值的 channel 取值表达式:
outer:
for {
select {
case d, ok := <-ch1:
if !ok {
break outer
}
case d, ok := <-ch2:
if !ok {
break outer
}
}
}
当然,如果你不确定,可以用下面的 demo 进行验证:
package main
import "time"
func main() {
var ch1 chan int
var ch2 = make(chan int)
close(ch2)
go func() {
for {
select {
case d := <-ch1:
println("ch1", d)
case d := <-ch2:
println("ch2", d)
}
}
}()
time.Sleep(time.Hour)
}
尽管 ch2 已经被关闭,依然会不断地进入 case d:= <-ch2 中。因此在使用 for select 做设计时,请务必考虑当监听的 channel 在外部被正常或意外关闭后会有什么样的后果。