化茧成蝶:Go在FreeWheel服务化中的实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

特殊状态下的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

try

从已关闭的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里。