分享
  1. 首页
  2. 文章

[go语言]channel的一个"奇怪"特性

stevewang · · 5865 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

go语言的channel有一个看上去很奇怪的特性,就是如果向一个为空值(nil)的channel写入或者读取数据,当前goroutine将永远阻塞。例如


func main() {
	var ch chan int
	ch <- 1 // block forerver } func main() { var ch chan int <-ch // block forerver } func main() { <-chan int(nil) // block forerver } func main() { chan int(nil)<-1 // block forerver } 

以上四个main函数都会永远阻塞(但是因为没有其他goroutine,所以runtime会报告一个deadlock错误)。

这看上去似乎是一个bug,因为向一个没有初始化的channel读或者写其实是没有意义的。但是为什么go team要这么设计呢?

关于这个问题在golang-nuts上有很多讨论,其中有一组讨论[1]说到其实这个特性(即对nil channel的读写永远阻塞)可以用来比较优雅地实现一个叫做"guarded selective wating"的模式,其实就是条件等待:在select中的一些case中如果对应的条件不满足就不在这个case上等待。假设有这样一个条件等待的需求:


select {
case <-chan_a: // 希望cond_a为真时才在chan_a上等待 // do something case <-chan_b: // 希望cond_b为真时才在chan_b上等待 // do something case <-chan_def: // do something } 

这个模式如果不利用这个特性,也是可以实现的,但是代码就比较冗长难看。其中一种实现可能是这样:


switch {
case cond_a && cond_b:
	select {
	case <-chan_a: // do something case <-chan_b: // do something case <-chan_def: // do something } case !cond_a && cond_b: select { case <-chan_b: // do something case <-chan_def: // do something } case cond_a && !cond_b: select { case <-chan_a: // do something case <-chan_def: // do something } default: select { case <-chan_def: // do something } } 

可以看到,实现代码非常的冗长罗嗦容易出错。而且如果case分支更多一些,实现代码的行数会以2的指数的数量爆炸性增长。当然还有其他实现方式,但如果不想办法去故意阻塞一个channel,实现的方法都是大同小异,都有前面说的问题。

但是如果用了nil channel特性,实现起来就可以非常的优雅简洁:


maybe := func(flag bool, ch chan int) <-chan int { if flag { return ch } return nil } select { case <- maybe(cond_a, chan_a): // do something case <- maybe(cond_b, chan_b): // do something case <- chan_def: // do something } 

这里实际是利用了nil channel永远阻塞的特性,但是如果我们创建一个channel,但是不向它写数据也不关闭它,而是只从它读数据,那么也是可以实现永远阻塞的。以下代码实现了同样的效果:


var _BLOCK = make(<-chan int) maybe := func(flag bool, ch chan int) <-chan int { if flag { return ch } return _BLOCK } select { case <- maybe(cond_a, chan_a): // do something case <- maybe(cond_b, chan_b): // do something case <- chan_def: // do something } 

这也就意味着:对于实现一个"guarded selective wating"模式来说,nil channel的永久阻塞的特性并不是必须的,因为有其他替代实现方式。但是显然用nil channel更方便,也不需要额外浪费资源去创建一个用来永久阻塞的channel。

一些争议:

有人说就算nil channel在select里很有用,但是在select之外单独去读写一个nil channel确实是个很奇怪的行为,无论如何看上去都是一个bug。runtime如果在这里产生一个panic而不是永久阻塞,就可以更好地告诉程序员说:嗨,你这里有个bug。如果是永久阻塞的话,这个bug就不会那么容易被注意到。

go team的成员回应说:这个是为了和在select里的行为一致,如果nil channel在select里永久阻塞而在其他地方panic,行为就不一致了,会让程序员感到疑惑;而且这也违反了go1的语言规范;另外读nil channel永久阻塞,和读一个没有数据的channel效果是一样的,如同遍历一个为空值的数组切片或者map和遍历一个长度为0的数组切片或者没有成员的map效果也是一样。

例如:

var s []int // 未初始化,s是一个空值
for k, v := range s {
	// do something
}

s := []int{} // 已初始化,s长度为0
for k, v := range s {
	// do something
}

这两段代码行为是一样的,循环体里的代码都不会执行到,也都不会panic。

我的看法是在select之外的读写nil channel确实是一个bug,至少也是不好的代码风格(如果真有人故意这么用的话)。但是它并不容易在实际中出现,因为我们在使用channel的时候通常是把声明和初始化放在一起的,所以不会是空值;或者channel作为struct的一个成员,声明和初始化是分离的,但是一般也会有一个函数来初始化这个结构。所以在实际编码中并不容易产生读写一个nil channel的bug,这不是一个严重的问题。

[1]https://groups.google.com/forum/?fromgroups=#!topic/golang-nuts/ChPxr_h8kUM

有疑问加站长微信联系(非本文作者)

本文来自:新浪博客

感谢作者:stevewang

查看原文:[go语言]channel的一个"奇怪"特性

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
5865 次点击
1 回复 | 直到 2018年07月19日 08:45:37
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏