在使用select语句的时候,我们首先需要注意下面几个事情。
- 有默认分支,那么无论涉及通道操作的表达式是否有阻塞,
select
语句都不会被阻塞。如果那几个表达式都阻塞了,或者说都没有满足求值的条件,那么默认分支就会被选中并执行。 - 没有加入默认分支,那么一旦所有的
case
表达式都没有满足求值条件,那么select
语句就会被阻塞。直到至少有一个case
表达式满足条件为止。 - 我们需要通过接收表达式的第二个结果值来判断通道是否已经关闭。一旦发现某个通道关闭了,我们就应该及时地屏蔽掉对应的分支或者采取其他措施。这对于程序逻辑和程序性能都是有好处的。
select
语句只能对其中的每一个case
表达式各求值一次。如果我们想连续或定时地操作其中的通道的话,就往往需要通过在for
语句中嵌入select
语句的方式实现。但这时要注意,简单地在select
语句的分支中使用break
语句,只能结束当前的select
语句的执行,而并不会对外层的for
语句产生作用。这种错误的用法可能会让这个for
语句无休止地运行下去。
1. for select 作用于未关闭的通道
1.1 没有 default 分之场景
先看下面代码
func main() {
ch := make(chan int, 3)
go func() {
time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
ch <- 1
}()
for {
select {
case v, ok := <-ch:
fmt.Printf("v=%v, ok=%v\n", v, ok)
time.Sleep(1 * time.Second)
}
fmt.Println("waiting")
}
}
执行代码输出结果如下:
v=1, ok=true
waiting
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/home/wohu/project/go/src/demo/demo.go:17 +0x82
exit status 2
从结果我们可以看到:
- 当通道中没有数据时,且
select
语句没有default
分之时,会一直阻塞在case
语句中; - 当通道中有数据时,
case
语句拿到通道里面的值之后,继续执行select
语句块之外的其余for
循环体语句; - 当通道里面的数据被取走之后,
case
语句一直等待从通道中取数据,但是一直没有数据发送过来就会造成死锁;
怎么避免这个死锁问题呢? 这就需要加上 default
分之了。
1.2 有 default 分之场景
查看下面代码
func main() {
ch := make(chan int, 3)
go func() {
time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
ch <- 1
}()
for {
select {
case v, ok := <-ch:
fmt.Printf("v=%v, ok=%v\n", v, ok)
time.Sleep(1 * time.Second)
default:
fmt.Println("通道没有数据")
time.Sleep(1 * time.Second)
}
fmt.Println("waiting")
}
}
运行代码输出结果如下:
通道没有数据
waiting
通道没有数据
waiting
v=1, ok=true
waiting
通道没有数据
waiting
通道没有数据
从结果我们可以看到:
- 当通道中没有数据时,走
default
分之,default
分之完成后会继续执行for
循环体的其它语句; - 当通道中有数据,则会执行对应的
case
分之; - 当通道再一次没有数据时,则继续会执行
default
分之和剩余的其它for
循环体语句,而且会一直死循环执行;
2. for select 作用于关闭的通道
2.1 对关闭的通道执行 case 会造成死循环
继续下面代码
func main() {
ch := make(chan int, 3)
go func() {
time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
ch <- 1
close(ch)
}()
for {
select {
case v, ok := <-ch:
fmt.Printf("v=%v, ok=%v\n", v, ok)
time.Sleep(1 * time.Second)
default:
fmt.Println("通道没有数据")
time.Sleep(1 * time.Second)
}
fmt.Println("waiting")
}
}
运行输出结果
通道没有数据
waiting
通道没有数据
waiting
v=1, ok=true
waiting
v=0, ok=false
waiting
v=0, ok=false
...
注意我们在前面已经将通道关闭,这个时候的 case
语句依然成立,所以会形成死循环执行这个 case
语句。
那么怎样能跳出这个死循环的 case
语句呢?
2.2 跳出死循环的 case 语句
要跳出这个死循环的 case
语句,我们需要在 case
中通过第二个参数判断 chan
是否关闭,如果关闭则通过 make(chan type)
来将关闭的 chan
置 nil
,当再次执行到 select
时,因为 chan
是 nil
会进入阻塞。
而 select
中如果任意某个分之可读(包括 default
),它就会被执行,其他被忽略。所以在有 default
分之场景时, select
会跳过这个阻塞 case
,去执行 default
分之,这样就可以避开这个死循环的 case
分之。
func main() {
ch := make(chan int, 3)
go func() {
time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
ch <- 1
close(ch)
}()
for {
select {
case v, ok := <-ch:
if !ok {
ch = make(chan int)
fmt.Println("通道已经关闭")
} else {
fmt.Printf("v=%v, ok=%v\n", v, ok)
time.Sleep(1 * time.Second)
}
default:
fmt.Println("通道没有数据")
time.Sleep(1 * time.Second)
}
fmt.Println("waiting")
}
}
输出结果如下:
通道没有数据
waiting
通道没有数据
waiting
v=1, ok=true
waiting
通道已经关闭
waiting
通道没有数据
waiting
...
会一直循环打印 default
分之的输出,那怎样跳出这个循环呢?
2.3 跳出 for select 循环语句
- 可以使用
goto
加lable
跳转到for
外面; - 可以设置一个额外的标记位,当
chan
关闭时,设置flag=true
,在for
的最后判断flag
决定是否break
;
我们采用第二种方案:
func main() {
ch := make(chan int, 3)
go func() {
time.Sleep(2 * time.Second) // 延迟往通道里里面发送数据
ch <- 1
close(ch)
}()
exitFlag := false
for {
select {
case v, ok := <-ch:
if !ok {
ch = make(chan int)
fmt.Println("通道已经关闭")
exitFlag = true
} else {
fmt.Printf("v=%v, ok=%v\n", v, ok)
time.Sleep(1 * time.Second)
}
default:
fmt.Println("通道没有数据")
time.Sleep(1 * time.Second)
}
if exitFlag {
fmt.Println("跳出循环")
break
}
fmt.Println("waiting")
}
}
输出结果
通道没有数据
waiting
通道没有数据
waiting
v=1, ok=true
waiting
通道已经关闭
跳出循环
由以上示例我们可以得出以下结论:
select
语句中如果任意某个case
的通道有值可读时,它就会被执行,其他case
会被忽略;- 如果没有
default
语句,select
将有可能阻塞,直到某个case
分之有值可以运行,所以select
里最好有一个default
,否则将有一直阻塞的风险;
如果 select
语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行。
仅当 select
语句中的所有 case
表达式都被求值完毕后,它才会开始选择候选分支。这时候,它只会挑选满足选择条件的候选分支执行。如果所有的候选分支都不满足选择条件,那么默认分支就会被执行。如果这时没有默认分支,那么 select
语句就会立即进入阻塞状态,直到至少有一个候选分支满足选择条件为止。一旦有一个候选分支满足选择条件,select
语句就会被唤醒,这个候选分支就会被执行。