這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
go語言的channel有一個看上去很奇怪的特性,就是如果向一個為空白值(nil)的channel寫入或者讀取資料,當前goroutine將永遠阻塞。例如
func main() {var ch chan intch <- 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 somethingcase <-chan_b: // 希望cond_b為真時才在chan_b上等待// do somethingcase <-chan_def:// do something}
這個模式如果不利用這個特性,也是可以實現的,但是代碼就比較冗長難看。其中一種實現可能是這樣:
switch {case cond_a && cond_b:select {case <-chan_a:// do somethingcase <-chan_b:// do somethingcase <-chan_def:// do something}case !cond_a && cond_b:select {case <-chan_b:// do somethingcase <-chan_def:// do something}case cond_a && !cond_b:select {case <-chan_a:// do somethingcase <-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 somethingcase <- maybe(cond_b, chan_b):// do somethingcase <- 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 somethingcase <- maybe(cond_b, chan_b):// do somethingcase <- 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長度為0for 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