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

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的一个“奇怪”特性

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。