为什么 Go 会有 nil channels
每个写过 Go 的人都知道 channels. 我们大多数人也知道 channels 的默认值是 nil. 但是我们很多人都不知道这个 nil 值是有用的. (译注: 老实说读到这篇文章之前我也认为 nil channels 没什么用, 不仅没用, 我还觉得对 nil channels 的写入操作应该 panic 才对, 就像对 nil map 的写入是 panic 一样, 甚至认为这是 Go 的设计不一致问题)
我从一个学习 Go 的开发者的 twitter 上得到了同样的问题, 他想知道是否 Go nil channels 的存在仅仅是为了完整性.
想知道是否有用是有意义的, 因为它们的行为表现的截然相反.
给定一个 nil channel c:
- <-c 从 c 接收将永远阻塞
- c <- v 发送值到 c 会永远阻塞
- close(c) 关闭 c 引发 panic
但我仍然坚持他们是有用的. 让我先介绍一个问题, 其解决方案起初看起来很明显, 但实际上并不像人们想象的那么容易, 实际上却从 nil channels 中受益.
合并 channels
如果你选择接受它, 你的任务是编写一个函数, 给定两个 channels a 和 b 返回一个相同类型的 channel c. a 或 b 中收到的每个元素都将发送给 c, 并且一旦 a 和 b 都关闭, c 也将被关闭.
一个辅助函数
在我们开始之前, 让我们编写一个函数来帮助我们测试我们的解决方案. 此函数返回一个 channel , 该 channel 最终将随机接收所有给定的值并通过关闭完成.
func asChan(vs ...int) <-chan int { |
此函数创建一个 channel c, 启动一个新的 goroutine, 将值发送到创建的 channel c, 最后返回 channel c.
在处理 channels 时这是很常见的模式, 因此在继续阅读之前, 请确保你了解它的工作原理.
让我们开始吧
由于我们没有对 a 或 b 的偏好, 所以我们将避免通过选择我们应该首先 range 哪个 channel 来创建偏好.
相反我们会保持这种对称性, 并且使用一个无限循环来 select 这两个 channels.
func merge(a, b <-chan int) <-chan int { |
这看起来不错, 让我们写一个快速测试并运行它.
func main() { |
这应该以某种顺序打印 1 到 8 并成功结束. 让我们看看发生了什么.
> go run main.go |
好吧, 很明显这不好, 因为该程序没有结束. 一旦它打印出从 1 到 8 的值, 它将开始永远打印 0.
处理关闭的 channels
如果我们从一个关闭的 channel 接收会发生什么 ? 我们会得到 channel 类型的默认值. 在我们的例子中, 类型是 int, 所以值是 0.
我们可以通过与 0 比较来检查是否 channel 已经关闭, 但是如果我们接收到的其中一个值是 0, 会怎么样 ? 相反, 我们可以使用 “v, ok” 语法:
v, ok := <- c |
当使用这个语法时, ok 是一个布尔值, 只要 channel 是开着的, 它就是 true. 知道这一点, 我们可以避免将多余的 0 发送给 c .
在某一点上我们也应该停止迭代, 所以让我们也跟踪两个 channels 何时关闭.
func merge(a, b <-chan int) <-chan int { |
这看起来可能有用 ! 让我们来运行它.
> go run main.go |
哎呀, 我们忘了一些事情. 是什么呢 ? 我们可以看到只有一个 goroutine 在运行, 并且它阻塞在第 13 行:
for v := range c { |
你能看出问题是什么吗 ? range 语句迭代 channel 中的所有值直到 channel 关闭. 但是谁关闭了这个 channel ?
我们忘了 ! 让我们在我们的 goroutine 中添加 defer 语句, 以确保该 channel 最终关闭.
func merge(a, b <-chan int) <-chan int { |
请注意, defer 语句位于新的 goroutine 中调用的匿名函数中, 而不是在 merge 中. 否则, 只要我们退出 merage, c 就会被关闭, 那么发送一个值给它将引发 panic.
让我们运行它, 看看会发生什么.
> go run main.go |
这看起来很棒 … 但是是这样吗 ?
繁忙的循环
我们迄今为止编写的代码非常好. 它在功能上是正确的, 但是如果你在生产中部署了它, 你最终可能会遇到性能问题.
为了向你显示问题所在, 让我们添加一些日志记录.
func merge(a, b <-chan int) <-chan int { |
让我们运行它, 看看会发生什么.
> go run main.go |
呃哦 ! 似乎一旦一个 channel 完成, 我们就不停地迭代 !
毕竟它确实有意义. 正如我们在开始时看到的, 从一个关闭的 channel 读取从不阻塞.
因此, 只要两个 channels 都处于打开状态, select 语句将会阻塞, 直到新元素准备就绪, 但是一旦其中一个关闭, 我们将迭代并浪费 CPU. 这也被称为繁忙的循环, 并不好.
在 select 语句中禁用一个 case
为了避免之前描述的繁忙循环, 我们希望禁用 select 语句的一部分. 具体来说, 当 a 关闭的时候我们想移除 (case v, ok := <- a), b 也一样. 但是怎么做呢 ?
正如我们在开头提到的那样, 从 nil channels 接收数据会永远阻塞. 所以为了禁用一个从 channel 接收数据的 case, 我们可以简单将 channel 设置为 nil !
然后, 我们可以停止使用 adone 和 bdone, 而是检查 a 和 b 是否为 nil .
func merge(a, b <-chan int) <-chan int { |
好, 希望这可以避免不必要的循环. 我们来试试吧.
> go run main.go |
最终解决方案的代码在 GitHub 上.