在本文发表数日前,我曾写了一篇文章来解释​​通道的规则​​​。 那篇文章在​​reddit​​​和​​HN​​上获得了很多点赞,但也有很多人对Go通道的细节设计提出了一些批评意见。

这些批评主要针对于通道设计中的下列细节:

  1. 没有一个简单和通用的方法用来在不改变一个通道的状态的情况下检查这个通道是否已经关闭。
  2. 关闭一个已经关闭的通道将产生一个恐慌,所以在不知道一个通道是否已经关闭的时候关闭此通道是很危险的。
  3. 向一个已关闭的通道发送数据将产生一个恐慌,所以在不知道一个通道是否已经关闭的时候向此通道发送数据是很危险的。


这些批评看上去有几分道理(实际上属于对通道的不正确使用导致的偏见)。 是的,Go语言中并没有提供一个内置函数来检查一个通道是否已经关闭。

在Go中,如果我们能够保证从不会向一个通道发送数据,那么有一个简单的方法来判断此通道是否已经关闭。 此方法已经在上一篇文章​​通道用例大全​​中展示过了。 这里为了本文的连贯性,在下面的例子中重新列出了此方法。


如前所述,此方法并不是一个通用的检查通道是否已经关闭的方法。

​closed​​closed(ch)​​true​​ch​​closed(ch)​​false​​ch​

通道关闭原则

一个常用的使用Go通道的原则是不要在数据接收方或者在有多个发送者的情况下关闭通道。 换句话说,我们只应该让一个通道唯一的发送者关闭此通道。

下面我们将称此原则为通道关闭原则

当然,这并不是一个通用的关闭通道的原则。通用的原则是不要关闭已关闭的通道。 如果我们能够保证从某个时刻之后,再没有协程将向一个未关闭的非nil通道发送数据,则一个协程可以安全地关闭此通道。 然而,做出这样的保证常常需要很大的努力,从而导致代码过度复杂。 另一方面,遵循通道关闭原则是一件相对简单的事儿。

粗鲁地关闭通道的方法

如果由于某种原因,你一定非要从数据接收方或者让众多发送者中的一个关闭一个通道,你可以使用​​恢复机制​​来防止可能产生的恐慌而导致程序崩溃。 下面就是这样的一个实现(假设通道的元素类型为



此方法违反了通道关闭原则

同样的方法可以用来粗鲁地向一个关闭状态未知的通道发送数据。

这样的粗鲁方法不仅违反了通道关闭原则,而且Go白皮书和标准编译器​​不保证​​​它的实现中不​​存在数据竞争​​。

礼貌地关闭通道的方法

​sync.Once​
​sync.Mutex​



​SafeClose​

优雅地关闭通道的方法

​SafeSend​​case​​select​
​sync.WaitGroup​​sync.WaitGroup​

情形一:M个接收者和一个发送者。发送者通过关闭用来传输数据的通道来传递发送结束信号

这是最简单的一种情形。当发送者欲结束发送,让它关闭用来传输数据的通道即可。



情形二:一个接收者和N个发送者,此唯一接收者通过关闭一个额外的信号通道来通知发送者不要在发送数据了

此情形比上一种情形复杂一些。我们不能让接收者关闭用来传输数据的通道来停止数据传输,因为这样做违反了通道关闭原则。 但是我们可以让接收者关闭一个额外的信号通道来通知发送者不要在发送数据了。



​stopCh​​dataCh​​dataCh​​stopCh​
​dataCh​


情形三:M个接收者和N个发送者。它们中的任何协程都可以让一个中间调解协程帮忙发出停止数据传送的信号

 这是最复杂的一种情形。我们不能让接收者和发送者中的任何一个关闭用来传输数据的通道,我们也不能让多个接收者之一关闭一个额外的信号通道。 这两种做法都违反了通道关闭原则

然而,我们可以引入一个中间调解者角色并让其关闭额外的信号通道来通知所有的接收者和发送者结束工作。 具体实现见下例。注意其中使用了一个尝试发送操作来向中间调解者发送信号。


在此例中,通道关闭原则依旧得到了遵守。

​toStop​​toStop​
​toStop​


情形四:“M个接收者和一个发送者”情形的一个变种:用来传输数据的通道的关闭请求由第三方发出

​dataCh​​dataCh​


情形五:“N个发送者”的一个变种:用来传输数据的通道必须被关闭以通知各个接收者数据发送已经结束了

​dataCh​​dataCh​​dataCh​