谈到select
,很多读者会想到网络编程中用于I/O
多路复用的select函数,在Go
语言中,select
语句有类似的功能。
在使用通道时,更多的时候会与select
结合,因为时常会出现多个通道与多个协程进行通信的情况,我们当然不希望由于一个通道的读写陷入堵塞,影响其他通道的正常读写。select
正是为了解决这一问题诞生的,select
赋予了Go
语言更加强大的功能。在使用方法上,select
的语法类似switch
,形式如下:
select {
case <-ch1:
// ...
case x := <-ch2:
// ...
case ch3 <- y:
// ...
default:
// ...
}
和switch
不同的是,每个case
语句都必须对应通道的读写操作。select
语句会陷入堵塞,直到一个或多个通道能够正常读写才恢复。
select
堵塞与控制如果select
中没有任何的通道准备好,那么当前select
所在的协程会永远陷入等待,直到有一个case
中的通道准备好为止:
ch := make(chan int, 1)
for {
select {
case <-ch:
fmt.Println("random 01")
case <-ch:
fmt.Println("random 02")
}
}
在实践中,为了避免这种情况发生,有时会加上default
分支。default
分支的作用是当所有的通道都陷入堵塞时,正常执行default
分支:
ch := make(chan int, 1)
for {
select {
case <-ch:
fmt.Println("random 01")
case <-ch:
fmt.Println("random 02")
default:
fmt.Println("default")
}
}
除了default
,还有一些其他的选择。例如,如果我们希望一段时间后仍然没有通道准备好则超时退出,可以选择select
与定时器或者超时器配套使用。
如下所示,<-time.After
(800*time.Millisecond
)调用了time
包的After
函数,其返回一个通道800ms
后会向当前通道发送消息,可以通过这种方式完成超时控制:
for {
select {
case <-ch:
fmt.Println("random 01")
case <-ch:
fmt.Println("random 02")
case <-time.After(800 * time.Millisecond):
fmt.Println("timeout")
}
}
select
很多时候,我们不希望select
执行完一个分支就退出,而是循环往复执行select
中的内容,因此需要将for
与select
进行组合,如上面的示例所示。
for
与select
组合后,可以向select中加入一些定时任务。下例中的tick每隔1s就会向tick通道中写入数据,从而完成一些定时任务:
func main() {
ch := make(chan int, 1)
tick := time.Tick(500 * time.Millisecond)
for {
select {
case <-ch:
fmt.Println("random")
case <-tick:
fmt.Println("tick")
case <-time.After(800 * time.Millisecond):
fmt.Println("default")
}
}
}
time.Tick
与time.After
是有本质不同的:time.After
并不会定时发送数据到通道中,而只是在时间到了后发送一次数据。当其放入for+select
后,新一轮的select
语句会重置time.After
,这意味着第2次select
语句依然需要等待800ms才执行超时。如果在800ms
之前,其他的通道就已经执行好了,那么time.After
的case
将永远得不到执行。而定时器tick
不同,由于tick
在for
循环的外部,因此其不重置,只会累积时间,实现定时执行任务的功能。select
与nil
一个为nil
的通道,不管是读取还是写入都将陷入堵塞状态。当select
语句的case
对nil
通道进行操作时,case
分支将永远得不到执行。
nil
通道的这种特性,可以用于设计一些特别的模式。例如,假设有ch1
、ch2
两个通道,我们希望交替地向ch1
、ch2
通道中发送消息,那么可以用如下方式:
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i < 2; i++ {
select {
case ch1 <- 1:
ch1 = nil
case ch2 <- 2:
ch2 = nil
}
}
}()
fmt.Println(<-ch1)
fmt.Println(<-ch2)
}
一旦写入通道后,就将该通道置为nil
,导致再也没有机会执行该case
。从而达到交替写入ch1
、ch2
通道的目的。