go并发之路(四)—— 确定通道是否关闭

1,456 阅读3分钟

本文同时参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前文回顾

所以目前我们发现

  • 错误的超时机制的设计让我们没办法正确运行
  • 为了做出改变,我们希望最好有确定court通道是否关闭的办法

所以本章我们主要讨论如何确定court通道是否关闭,在此之前,我们不妨设想下,没有这个功能会给我造成什么样子的麻烦

  • 在不能更改channel状态的情况下,没有简单普遍的方式来检查channel是否已经关闭了

  • 关闭已经关闭的channel会导致panic,在不知道channel是否已经关闭的情况下去关闭channel是很危险的

  • 发送值到已经关闭的channel会导致panic,在不知道channel是否已经关闭的情况下去向channel发送值也是很危险的

读取数据时

每次从通道读取数据时,都可以读到通道本身是否关闭的状态,因此在读取数据时,我们可以轻松判断court通道是否关闭。

package main
import (
    "fmt"
)
func main() {
    c := make(chan int, 10)
    c <- 1
    c <- 2
    c <- 3
    close(c)
    for {
        i, isClose := <-c
        if !isClose {
            fmt.Println("channel closed!")
            break
        }
        fmt.Println(i)
    }
}

写入数据时

我们期待这类似的功能,一个简单的函数让我们可以监测一个通道的状态

func main() {
    c := make(chan T)
    fmt.Println(IsClosed(c)) // false
    c <- DATA
    close(c)
    fmt.Println(IsClosed(c)) // true
}

然而这中isClosed函数往往作用不大,(这也是为什么不会有这类函数的原因),因为就算有一个简单的 closed(chan T) bool函数来检查channel是否已经关闭,它的用处还是很有限的,就像内置的len函数用来检查缓冲channel中元素数量一样。原因就在于,已经检查过的channel的状态有可能在调用了类似的方法返回之后就修改了,因此返回来的值已经不能够反映刚才检查的channel的当前状态了。

尽管在调用closed(ch)返回true的情况下停止向channel发送值是可以的,但是如果调用closed(ch)返回false,那么关闭channel或者继续向channel发送值就不安全了(这样可能会造成panic,因为之前未关闭的通道可能会在此刻恰好关闭)。

使用通道的原则

事实上,go并不建议我们由接受者关闭通道,或者在有多个发送者时由其中一个发送者关闭通道,维持这条原则将保证永远不会发生向一个已经关闭的channel发送值或者关闭一个已经关闭的channel。

那么如何安全的打破这条原则?

我们可以使用panic/recover()模式来向通道中写入数据:

func SafeWrite(ch chan T, value T) (isClosed bool) {
    defer func() {
        if recover() != nil {
            // 在panic后,会进入这个if中,然后将返回值改变为true,代表通道是关闭的
            isClosed = true
        }
    }()
    
    ch <- value // 如果通道关闭,这里会报panic
    return false // 如果可以正常写入,这里会返回false,代表通道不是关闭的
}