特殊状态下的Channel数据读写
以上关于Channel的数据写入和读取的说明及示例都是基于正常状态(已初始化,未被关闭)下的Channel。如果一个Channel被关闭,或者Channel为零值nil(未被初始化,被赋值为nil),其读写的行为都有所不同。说到这里,我们总结一下Channel在几种特殊状态下读写数据的情况。这部分内容对于用对用好Channel这一武器,避免程序出现非预期的状态非常重要,也是Dave Cheney在他的博客里总结过的。
关闭Channel即:
close(ch)
但Channel的关闭是不应当随意进行的,因为关闭Channel后的读和写的行为都需要我们深刻理解,才能选择在程序适当的地方去做Channel的关闭操作。
首先,已关闭的Channel永远不会阻塞。很好,不阻塞。至少在错误状态出现的时候程序不会Block,错误比较容易被发现。上面刚刚说过Channel的关闭,那么对于已关闭的Channel的读写操作会是什么样的状况,又如何避免错误使用带来的非预期结果呢?
写入已关闭的Channel
注意,向已关闭的Channel写入会panic。例如这段代码,在关闭Channel后再往里写入值
ch1 := make(chan string, 1) go func() { v := <- ch1 fmt.Println("Received from ch1: ", v) }() close(ch1) ch1 <- "Hi, nice to meet you" fmt.Println("Done")
会输出
panic: send on closed channel
从已关闭的Channel读取
Channel关闭后从其中读取会是什么样的行为?一个好消息是,Channel关闭后,读取操作就再也不会Block了。是不是感觉松一口气?对于无Buffer的Channel,关闭后再读取,不会Block,会读取到该Channel类型的零值。具体来讲,就是如果这个Channel是String类型的,就会读到"", 如果Channel是interface{}类型,将会读到空指针nil,以此类推。
对于Buffered Channel,关闭后读取操作依然可以把其中的数据读取出来,直到这个Channel被取空。当所有值都读完后,继续读该Channel会得到该Channel的类型的零值数据。
所以在这里我们可以很清楚地推断出,如果用select无限循环地读取,那么Channel被关闭后就会死循环了,永远能读到值,但每次取到的都是该Channel类型的零值。而如果用for...range方式来读取的话,循环就会在Channel中的数据被全部取出后结束。还有一种方式可以用来判断已关闭的Channel是否已经被取空,就是读取Channel时使用读取操作的第二个返回值,如下所示
x, ok := <-ch
当Channel被关闭并且为空的时候,ok为false。下面看看两种情况的例子。
用select读取
用常用的select方式读取已关闭的Channel会是怎样的行为?如上所述,Channel被关闭后不会Block,会永远返回零值,所以如果在代码里不做判断的话将进入无限循环状态。还是用上面那段代码做例子
func main() { ch1 := make(chan int) go func() { for { select { case val := <-ch1: fmt.Println("Received value: ", val) } } }() for i := 0; i <= 10; i++ { ch1 <- i * 2 } fmt.Println("Process done.") }
没问题,正常输出了0-10的2倍
Received value: 0 Received value: 2 Received value: 4 Received value: 6 Received value: 8 Received value: 10 Received value: 12 Received value: 14 Received value: 16 Received value: 18 Received value: 20 Process done.
如果在发送完毕后关闭Channel, 循环读取将进入无限循环状态
close(ch1) fmt.Println("Process done.")
输出将变为:
... ... Received value: 0 Received value: 0 Received value: 0 Received value: 0 Received value: 0 Received value: 0 Received value: 0 Received value: 0 ......
死循环,无论有Buffer还是无Buffer的Channel都一样。所以如果用select无限循环方式作为Channel接收的方式,可以利用读Channel的第二个返回值加一个判断来处理Channel关闭并被读取完的行为
func main() { ch1 := make(chan int) go func() { var closed bool for { if closed { break } select { case val, ok := <-ch1: fmt.Println("Received value: ", val) if ! ok { closed = true break } } } fmt.Println("Channel is closed and drain out.") }() for i := 0; i <= 10; i++ { ch1 <- i * 2 } close(ch1) fmt.Println("Process done.") }
这时候的输出是
Received value: 0 Received value: 2 Received value: 4 Received value: 6 Received value: 8 Received value: 10 Received value: 12 Received value: 14 Received value: 16 Received value: 18 Received value: 20 Process done. Received value: 0 Channel is closed and drain out.
使用有Buffer的Channel时,行为也是类似的。
用for循环读取
在Channel被关闭后,for...range方式就比较适合用来Drain out Channel中的所有数据了,下面这段代码在发送完毕数据后关闭Channel,而读取方的for...range循环会在接收完所有数据之后退出循环。
func main() { ch1 := make(chan int, 20) for i := 0; i <= 10; i++ { ch1 <- i * 2 } close(ch1) go func() { l := len(ch1) fmt.Println("-- start read, the length of channel is: ", l) for r := range ch1 { fmt.Printf("Received value: %d, the length is: %d\n", r, len(ch1)) } fmt.Println("Drain channel out done.") }() fmt.Println("Process done.") }
输出为:
Process done. -- start read, the length of channel is: 11 Received value: 0, the length is: 10 Received value: 2, the length is: 9 Received value: 4, the length is: 8 Received value: 6, the length is: 7 Received value: 8, the length is: 6 Received value: 10, the length is: 5 Received value: 12, the length is: 4 Received value: 14, the length is: 3 Received value: 16, the length is: 2 Received value: 18, the length is: 1 Received value: 20, the length is: 0 Drain channel out done.
基于Channel关闭后的行为,如何关闭它才是安全的呢?有一条比较通用的适用原则,即不要从接收端关闭Channel。如果发送端有多个并发发送者,也不要从其中一个发送端去关闭Channel。换句话说,如果发送者是唯一的Sender,或者发送者是Channel最后一个活跃的Sender,那么应该在发送者的Goroutine关闭Channel,通过这个关闭行为来通知所有的接收者们,已经没有值可以读了。只要在使用Channel时遵循这条原则,就可以保证永远不会发生向一个已经关闭的Channel写入值,或者关闭一个已经关闭的Channel这样的情况。
写入值为nil的Channel
当一个Channel的值为nil时,写入这个Channel的操作会永远阻塞。例如下面这段代码:
func main() { var c chan string c <- "hello, David" // block forever }
从值为nil的Channel读取
同样的,从一个nil的Channel读取,也会永远阻塞:
func main() { var c chan string str := <- c // block forever }
一个声明后但尚未通过make语句初始化的Channel的默认值就是其零值,也就是nil。所以在使用Channel时一定要注意这一点,在对其写入或者读取时一定要保证这个Channel已经被初始化了。阻塞是一种可怕的安静,没有任何错误,没有panic,进程还活着,但是当前Goroutine陷入了无尽的等待中,无法继续往下走,无法感知到。
从Channel的原理上来看,如何解释这种阻塞行为?Dave Cheney很好地给出了他的解释,我来简单描述一下:
首先,Channel的类型定义并不包括Buffer,所以Buffer是Channel本身的值的一部分,Channel的声明只会声明其类型。Channel只是被声明而没有被初始化的时候,它的Buffer size是0。这种情况下相当于是一个无Buffer的Channel了,只有在发送和接收双方都准备好的情况下,无Buffer的Channel才不会阻塞。而对于未初始化的Channel,其值是nil,发送和接收双方都无法获取到对方的引用,无法和对方通讯,双方都被阻塞在这个未初始化的Channel里。