深入 Golang channel 基礎用法都在這了!
1. 前言
你好哇!本文是「Golang 併發編程」系列的第 2 篇文章~
現在感覺這個坑開得有點大,沒個一年半載的講不清楚了……
上篇文章我們學習了 Go 語言中的 Goroutine 創建和調度的機制,理解了一條簡單的 go
命令背後的原理。本文開始,將深入研究 go 語言中的 channel,將拆分爲基礎用法、實用場景、反面教材、源碼實現四篇文章來介紹,歡迎關注追更~
本文將介紹 channel 相關的概念、語法和規則,不涉及原理和源碼分析,更深入的內容,後面的更新會覆蓋到,敬請期待~
2. Channel 簡介
Channel 是 go 語言內置的一個非常重要的特性,也是 go 併發編程的兩大基石之一(另一個是 go
,也就是 goroutine )。Channel 也是 go 裏面非常有趣的一個功能,有時候甚至有點燒腦,相對於傳統的 mutex
等同步併發原語,掌握 channel 門檻更高、更容易出錯,因此更值得深入學習。
關於併發編程,Rob Pike 有個名言:
不要通過共享內存來通信,要通過通信來共享內存
Don't (let computations) communicate by sharing memory, (let them) share memory by communicating (through channels)
在 go 語言中,channel 就是 goroutine 之間通過通信來共享內存的手段。可以把 channel 看作 go 程序內部的一個 FIFO (first in, first out) 隊列,一些 goroutine 向其中生產數據,另外一些消費數據。
另外 channel 是 go 語言中的一等公民,不像使用 mutex
、waitgroup
等其它併發編程原語需要引入 sync
、atomic
等包,channel 可以直接使用,不需要引入任何包。
3. Channel 的類型和值
跟 slice 、map 這些內置類型一樣,channel 作爲一種元素類型,也是有具體的類型的,channel 只能傳遞聲明的類型的值。
基礎類型:雙向與單向 channel
對於類型 T
,可以聲明三種類型的 channel:
-
chan T
雙向 channel ,既能接收值又能發送值 -
chan<- T
send-only channel,只能往裏寫(chan
是箭頭的終點) -
<-chan T
receive-only channel,只能從裏讀(chan
是箭頭的起點) 在函數中使用即:
func foo(ch1 <-chan int) // 只能從 ch1 裏讀
func bar(ch2 chan<- int) // 只能往 ch2 裏寫
雙向 channel chan T
可以被隱式轉換成 send-only channel chan<- T
或 receive-only channel <-chan T
;而 chan<- T
與 <-chan T
兩者則不能互相轉換(顯式轉換也不行)
單向 channel 是一種函數傳參時的安全性約束,在實際使用中幾乎不可能單獨去聲明一個單向的 channel。
buffered channel + unbuffered channel + nil channel
每個 channel 類型的值都會有一個容量(capacity),根據 capacity 大小來區分,可以分爲兩種:
-
buffered channel:帶緩衝的 channel,cap > 0
-
unbuffered channel:不帶緩衝的 channel,cap = 0
使用 make 創建 channel:
ch1 := make(chan int, 10) // buffered channel, cap = 10
ch2 := make(chan int) // unbuffered channel, cap = 0 (make chan 函數第二參數默認值爲 0)
var ch3 chan int // nil 是 chan 的零值(zero value)
注意到上面的例子中還有一種特殊的 channel :nil channel
,在只聲明但是並未 make
時,channel 的值是零值 nil。nil channel 無論是寫入還是讀取都會永久阻塞住。
4. channel 的 7 種操作
1. 向 channel 發送值
ch <- v
需要注意:
-
v
需要和ch
聲明的元素類型相同 -
<-
是channel-send
即 channel 發送操作符 -
這裏的
ch
不能是 receive-only channel
2. 從 channel 裏讀取結果
<- ch
-
從 channel 裏取數據的操作符叫
channel-receive
操作符,使用它總會有至少一個返回值,它跟channel-send
操作符長得一樣 -
這裏的
ch
不能是 send-only channel -
channel-receive
操作符大部分時候都返回一個結果,但是也能返回兩個結果,第二個結果是用來指示從 channel 裏出來的值是否是在 channel 關閉之前讀到的,如以下代碼所示:
v = <-ch
v, sentBeforeClosed = <-ch // 先關閉再發送 v,則返回 false
這種方式與操作 map 時的方式類似,被稱作 ok-idiom
,並且,在 close(ch)
函數執行的時候,會對 ch 發送一條消息,這個動作可以用來通知所有的 goroutine 退出,例如:
package main
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for {
x, ok := <-c
if !ok { // close 時會收到一條消息,x 值爲 0,ok 爲 false
return
}
println(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<-done // close 時會收到消息,解除阻塞
}
3. for-range 操作
使用 for-range
遍歷 channel 會比使用 ok-idiom
更簡潔,將上面的例子用 for-range
的方式來實現:
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for x := range c {
println(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<-done // close 時會收到消息,解除阻塞
}
4. select 多路選擇
將在下文專門討論
5. 關閉 channel
close(ch)
close
是個 go 內置函數,只能操作 channel 類型,且 close 的對象不能是 receive-only channel。另外,在前面的例子裏可以看到, close 發生時,會向被關閉的 channel 發送一條消息,解除阻塞,這個特性可以用來做一些一次性的操作。
錯誤的 close 會引發程序的 panic,關於如何優雅關閉 channel,我會在後面的「反面教材:panic 和內存泄漏」主題裏展開,敬請期待~繼續挖坑……
6. 返回 channel 的容量(capacity)
cap(ch)
cap
是 go 的內置函數,會返回一個 int
類型的 channel 容量,可以參考作用在 slice 時的表現
7. 返回 channel buffer 中值的數量
len(ch)
跟 cap
函數類似,len
也是 go 語言的內置函數,返回值是已經成功寫入 channel buffer 但是還沒有從 channel 裏讀出來的值的數量。在 channel 的操作中,cap
和 len
函數其實用得都不多。
5. 阻塞場景梳理
針對根據 channel 是否爲空和是否關閉,可以分成以下三類來討論:
-
空 channel (nil channel)
-
非空已關閉 channel
-
非空未關閉 channel
要理解這幾種現象,就要看下 channel 的內部結構了,可以認爲 channel 內部有三個 FIFO 隊列
-
接收數據的 goroutine 隊列,是一個無限長的鏈表,這個隊列裏的 goroutine 都處於阻塞狀態,等待數據從 channel 寫入
-
發送數據的 goroutine 隊列,也是一個無限長的鏈表,這個隊列裏的 goroutine 都處於阻塞狀態,等待數據向 channel 寫入。每個 goroutine 嘗試發送的值也和 goroutine 一起存在這個隊列裏
-
值 buffer 隊列,是一個環形隊列(ringbuffer),它的大小跟 channel 的容量相同。存在這個 buffer 隊列裏的值跟 channel 元素的類型相同。如果當前 buffer 隊列裏儲存的值的數量達到了 channel 的容量,這個 channel 就「滿了」,對於 unbuffered channel 而言,它總是既在「空」狀態,又在「滿」狀態。
更多的原理性的分析,將在本系列的後續「原理與源碼分析」文章展開。
6. 多路選擇操作符 select
select 語句是專門爲 channel 操作而設計的,使用時類似 switch-case
的用法,適用於處理多通道的場景,會通過類似 are-you-ready-polling
的機制來工作。當多個 case 中有一個準備好了,就能執行,無論是收還是發。
阻塞與非阻塞 select
select 默認是阻塞的,當沒有 case 處於激活狀態時,會一直阻塞住,極端的甚至可以這樣用...
select {
// 啥也不幹,一直阻塞住
}
通過增加 default
,可以實現非阻塞的 select
select {
case x, ok := <-ch1:
...
case ch2 <- y:
...
default:
fmt.Println("default")
}
多 case 與 default 執行的順序
整體流程如圖所示:
-
隨機性:多個 case 之間並非順序的,遵循「先到先執行,同時到則隨機執行」的原則
-
一次性:和
switch-case
一樣,select-case
也只會執行一次,如果需要多次處理,需要在外層套一個循環 -
default 不會阻塞,會一直執行,當與 for 循環組合使用時可能出現死循環,如下面代碼所示:
func main() {
var ch chan int
i := 0
for {
select {
case <-ch: // nil channel 永遠阻塞
fmt.Println("never...")
default:
fmt.Printf("in default, i = %d\n", i)
}
i++
}
}
小結
本文完整介紹了 channel 的所有基礎用法,包括 <-
、ok-idiom
、select-case
、close
、for-range
等,並分析了阻塞與非阻塞的場景。後面三篇文章將分別介紹 channel 在實際的程序世界中的多種實用案例、底層實現及使用不當導致的死鎖、內存泄漏等問題,敬請期待!
翔叔架構筆記 專注 Golang 後端開發、架構、大數據、職場發展等,立志輸出優質原創內容;關於翔叔:鵝廠長大的 T10,5 年互聯網後端服務與架構設計研發經驗,擅長社交領域、數據中臺等系統的設計與實現。希望與你共同成長 :)
參考資料
-
《Go 101 - Channels in Go》
-
legendtkl -《深入理解 Go Channel》
-
雨痕 -《Go 語言學習筆記》第 8 章,併發
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/m_Nd_3pW9MkYic6Ds52GMQ