
谈到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通道的目的。