這篇文章總結了channel的11種常用操作,以一個更高的視角看待channel,會給大家帶來對channel更全面的認識。
在介紹11種操作前,先簡要介紹下channel的使用場景、基本操作和注意事項。
channel的使用場景
把channel用在數據流動的地方:
- 消息傳遞、消息過濾
- 信號廣播
- 事件訂閱與廣播
- 請求、響應轉發
- 任務分發
- 結果匯總
- 並發控制
- 同步與異步
- …
channel的基本操作和注意事項
channel存在3種狀態
:
- nil,未初始化的狀態,只進行了聲明,或者手動賦值為
nil
- active,正常的channel,可讀或者可寫
- closed,已關閉,千萬不要誤認為關閉channel后,channel的值是nil
channel可進行3種操作
:
- 讀
- 寫
- 關閉
把這3種操作和3種channel狀態可以組合出9種情況
:
操作 | nil的channel | 正常channel | 已關閉channel |
---|---|---|---|
<- ch | 阻塞 | 成功或阻塞 | 讀到零值 |
ch <- | 阻塞 | 成功或阻塞 | panic |
close(ch) | panic | 成功 | panic |
對於nil通道的情況,也並非完全遵循上表,有1個特殊場景:當nil
的通道在select
的某個case
中時,這個case會阻塞,但不會造成死鎖。
參考代碼請看:https://dave.cheney.net/2014/03/19/channel-axioms
下面介紹使用channel的10種常用操作。
1. 使用for range讀channel
場景
當需要不斷從channel讀取數據時。
原理
使用for-range
讀取channel,這樣既安全又便利,當channel關閉時,for循環會自動退出,無需主動監測channel是否關閉,可以防止讀取已經關閉的channel,造成讀到數據為通道所存儲的數據類型的零值。
用法
1 |
for x := range ch{ |
2. 使用v,ok := <-ch
+ select
操作判斷channel是否關閉
場景
v,ok := <-ch
+ select
操作判斷channel是否關閉
原理
ok的結果和含義:
- `true`:讀到通道數據,不確定是否關閉,可能channel還有保存的數據,但channel已關閉。
- `false`:通道關閉,無數據讀到。
從關閉的channel讀值讀到是channel所傳遞數據類型的零值,這個零值有可能是發送者發送的,也可能是channel關閉了。
_, ok := <-ch
與select配合使用的,當ok為false時,代表了channel已經close。下面解釋原因,_,ok := <-ch
對應的函數是func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
,入參block含義是當前goroutine是否可阻塞,當block為false代表的是select操作,不可阻塞當前goroutine的在channel操作,否則是普通操作(即_, ok
不在select中)。返回值selected代表當前操作是否成功,主要為select服務,返回received代表是否從channel讀到有效值。它有3種返回值情況:
- block為false,即執行select時,如果channel為空,返回(false,false),代表select操作失敗,沒接收到值。
- 否則,如果channel已經關閉,並且沒有數據,ep即接收數據的變量設置為零值,返回(true,false),代表select操作成功,但channel已關閉,沒讀到有效值。
- 否則,其他讀到有效數據的情況,返回(true,ture)。
我們考慮_, ok := <-ch
和select
結合使用的情況。
情況1:當chanrecv返回(false,false)時,本質是select操作失敗了,所以相關的case會阻塞,不會執行,比如下面的代碼:
1 |
func main() { |
情況2:下面的結果會是零值和false:
1 |
func main() { |
情況3的received
為true,即_, ok
中的ok
為true,不做討論了,只討論ok
為false的情況。
最后ok
為false的時候,只有情況2,此時channel必然已經關閉,我們便可以在select
中用ok
判斷channel是否已經關閉。
用法
下面例子展示了,向channel寫數據然后關閉,依然可以從已關閉channel讀到有效數據,但channel關閉且沒有數據時,讀不到有效數據,ok為false,可以確定當前channel已關閉。
1 |
// demo_select6.go |
更多見golang_step_by_step/channel/ok倉庫中ok和select的示例,或者閱讀channel源碼。
3. 使用select處理多個channel
場景
需要對多個通道進行同時處理,但只處理最先發生的channel時
原理
select
可以同時監控多個通道的情況,只處理未阻塞的case。當通道為nil時,對應的case永遠為阻塞,無論讀寫。特殊關注:普通情況下,對nil的通道寫操作是要panic的。
用法
1 |
// 分配job時,如果收到關閉的通知則退出,不分配job |
4. 使用channel的聲明控制讀寫權限
場景
協程對某個通道只讀或只寫時
目的:
- 使代碼更易讀、更易維護,
- 防止只讀協程對通道進行寫數據,但通道已關閉,造成panic。
用法
- 如果協程對某個channel只有寫操作,則這個channel聲明為只寫。
- 如果協程對某個channel只有讀操作,則這個channe聲明為只讀。
1 |
// 只有generator進行對outCh進行寫操作,返回聲明 |
5. 使用緩沖channel增強並發
場景
異步
原理
有緩沖通道可供多個協程同時處理,在一定程度可提高並發性。
用法
1 |
// 無緩沖 |
1 |
// 使用5個`do`協程同時處理輸入數據 |
6. 為操作加上超時
場景
需要超時控制的操作
原理
使用select
和time.After
,看操作和定時器哪個先返回,處理先完成的,就達到了超時控制的效果
用法
1 |
func doWithTimeOut(timeout time.Duration) (int, error) { |
7. 使用time實現channel無阻塞讀寫
場景
並不希望在channel的讀寫上浪費時間
原理
是為操作加上超時的擴展,這里的操作是channel的讀或寫
用法
1 |
func unBlockRead(ch chan int) (x int, err error) { |
注:time.After等待可以替換為default,則是channel阻塞時,立即返回的效果
8. 使用close(ch)
關閉所有下游協程
場景
退出時,顯示通知所有協程退出
原理
所有讀ch
的協程都會收到close(ch)
的信號
用法
1 |
func (h *Handler) Stop() { |
9. 使用chan struct{}
作為信號channel
場景
使用channel傳遞信號,而不是傳遞數據時
原理
沒數據需要傳遞時,傳遞空struct
用法
1 |
// 上例中的Handler.stopCh就是一個例子,stopCh並不需要傳遞任何數據 |
10. 使用channel傳遞結構體的指針而非結構體
場景
使用channel傳遞結構體數據時
原理
channel本質上傳遞的是數據的拷貝,拷貝的數據越小傳輸效率越高,傳遞結構體指針,比傳遞結構體更高效
用法
1 |
reqCh chan *Request |
11. 使用channel傳遞channel
場景
使用場景有點多,通常是用來獲取結果。
原理
channel可以用來傳遞變量,channel自身也是變量,可以傳遞自己。
用法
下面示例展示了有序展示請求的結果,另一個示例可以見另外文章的版本3。