總結了才知道,原來channel有這麼多用法!
這篇文章總結了channel的10種常用操作,以一個更高的視角看待channel,會給大家帶來對channel更全面的認識。
在介紹10種操作前,先簡要介紹下channel的使用場景、基本操作和注意事項。
channel的使用場景
把channel用在 資料流動的地方 :
- 訊息傳遞、訊息過濾
- 訊號廣播
- 事件訂閱與廣播
- 請求、響應轉發
- 任務分發
- 結果彙總
- 併發控制
- 同步與非同步
- ...
channel的基本操作和注意事項
channel存在 3種狀態
:
- nil,未初始化的狀態,只進行了宣告,或者手動賦值為
nil
- active,正常的channel,可讀或者可寫
- closed,已關閉, 千萬不要誤認為關閉channel後,channel的值是nil
channel可進行 3種操作
:
- 讀
- 寫
- 關閉
把這3種操作和3種channel狀態可以組合出 9種情況
:

image
對於nil通道的情況,也並非完全遵循上表, 有1個特殊場景 :當 nil
的通道在 select
的某個 case
中時,這個case會阻塞,但不會造成死鎖。
參考程式碼請看: https://dave.cheney.net/2014/03/19/channel-axioms
下面介紹使用channel的10種常用操作。
1. 使用for range讀channel
for-range
for x := range ch{ fmt.Println(x) }
2. 使用 _,ok
判斷channel是否關閉
- 場景:讀channel,但不確定channel是否關閉時
- 原理:讀已關閉的channel會造成panic,如果不確定channel,需要使用
ok
進行檢測。ok的結果和含義:true false
- 用法:
if v, ok := <- ch; ok { fmt.Println(v) }
3. 使用select處理多個channel
- 場景:需要對多個通道進行同時處理,但只處理最先發生的channel時
- 原理:
select
可以同時監控多個通道的情況,只處理未阻塞的case。 當通道為nil時,對應的case永遠為阻塞,無論讀寫。特殊關注:普通情況下,對nil的通道寫操作是要panic的 。 - 用法:
// 分配job時,如果收到關閉的通知則退出,不分配job func (h *Handler) handle(job *Job) { select { case h.jobCh<-job: return case <-h.stopCh: return } }
4. 使用channel的宣告控制讀寫許可權
- 場景:協程對某個通道只讀或只寫時
- 目的:A. 使程式碼更易讀、更易維護,B. 防止只讀協程對通道進行寫資料,但通道已關閉,造成panic。
- 用法:
- 如果協程對某個channel只有寫操作,則這個channel宣告為只寫。
- 如果協程對某個channel只有讀操作,則這個channe宣告為只讀。
// 只有generator進行對outCh進行寫操作,返回宣告 // <-chan int,可以防止其他協程亂用此通道,造成隱藏bug func generator(int n) <-chan int { outCh := make(chan int) go func(){ for i:=0;i<n;i++{ outCh<-i } }() return outCh } // consumer只讀inCh的資料,宣告為<-chan int // 可以防止它向inCh寫資料 func consumer(inCh <-chan int) { for x := range inCh { fmt.Println(x) } }
5. 使用緩衝channel增強併發和非同步
- 場景:非同步和併發
- 原理:A. 有緩衝通道是非同步的,無緩衝通道是同步的,B. 有緩衝通道可供多個協程同時處理,在一定程度可提高併發性。
- 用法:
// 無緩衝,同步 ch1 := make(chan int) ch2 := make(chan int, 0) // 有緩衝,非同步 ch3 := make(chan int, 1)
// 使用5個`do`協程同時處理輸入資料 func test() { inCh := generator(100) outCh := make(chan int, 10) for i := 0; i < 5; i++ { go do(inCh, outCh) } for r := range outCh { fmt.Println(r) } } func do(inCh <-chan int, outCh chan<- int) { for v := range inCh { outCh <- v * v } }
6. 為操作加上超時
- 場景:需要超時控制的操作
- 原理:使用
select
和time.After
,看操作和定時器哪個先返回,處理先完成的,就達到了超時控制的效果 - 用法:
func doWithTimeOut(timeout time.Duration) (int, error) { select { case ret := <-do(): return ret, nil case <-time.After(timeout): return 0, errors.New("timeout") } } func do() <-chan int { outCh := make(chan int) go func() { // do work }() return outCh }
7. 使用time實現channel無阻塞讀寫
- 場景:並不希望在channel的讀寫上浪費時間
- 原理:是為操作加上超時的擴充套件,這裡的操作是channel的讀或寫
- 用法:
func unBlockRead(ch chan int) (x int, err error) { select { case x = <-ch: return x, nil case <-time.After(time.Microsecond): return 0, errors.New("read time out") } } func unBlockWrite(ch chan int, x int) (err error) { select { case ch <- x: return nil case <-time.After(time.Microsecond): return errors.New("read time out") } }
注:time.After等待可以替換為default,則是channel阻塞時,立即返回的效果
8. 使用 close(ch)
關閉所有下游協程
- 場景:退出時,顯示通知所有協程退出
- 原理:所有讀
ch
的協程都會收到close(ch)
的訊號 - 用法:
func (h *Handler) Stop() { close(h.stopCh) // 可以使用WaitGroup等待所有協程退出 } // 收到停止後,不再處理請求 func (h *Handler) loop() error { for { select { case req := <-h.reqCh: go handle(req) case <-h.stopCh: return } } }
9. 使用 chan struct{}
作為訊號channel
- 場景:使用channel傳遞訊號,而不是傳遞資料時
- 原理:沒資料需要傳遞時,傳遞空struct
- 用法:
// 上例中的Handler.stopCh就是一個例子,stopCh並不需要傳遞任何資料 // 只是要給所有協程傳送退出的訊號 type Handler struct { stopCh chan struct{} reqCh chan *Request }
10. 使用channel傳遞結構體的指標而非結構體
- 場景:使用channel傳遞結構體資料時
- 原理: channel本質上傳遞的是資料的拷貝,拷貝的資料越小傳輸效率越高,傳遞結構體指標,比傳遞結構體更高效
- 用法:
reqCh chan *Request // 好過 reqCh chan Request
推薦閱讀
本文介紹的channel特性,大多在過去的文章中已詳細介紹,可按需求閱讀。