這篇文章總結了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種情況
:
對於nil通道的情況,也並非完全遵循上表,有1個特殊場景:當nil
的通道在select
的某個case
中時,這個case會阻塞,但不會造成死鎖。
參考程式碼請看:https://dave.cheney.net/2014/...
下面介紹使用channel的10種常用操作。
1. 使用for range讀channel
- 場景:當需要不斷從channel讀取資料時
- 原理:使用
for-range
讀取channel,這樣既安全又便利,當channel關閉時,for迴圈會自動退出,無需主動監測channel是否關閉,可以防止讀取已經關閉的channel,造成讀到資料為通道所儲存的資料型別的零值。 - 用法:
for x := range ch{
fmt.Println(x)
}
2. 使用_,ok
判斷channel是否關閉
- 場景:讀channel,但不確定channel是否關閉時
-
原理:讀已關閉的channel會得到零值,如果不確定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增強併發
- 場景:併發
- 原理:有緩衝通道可供多個協程同時處理,在一定程度可提高併發性。
- 用法:
// 無緩衝
ch1 := make(chan int)
ch2 := make(chan int, 0)
// 有緩衝
ch3 := make(chan int, 1)
func test() {
inCh := generator(100)
outCh := make(chan int, 10)
// 使用5個`do`協程同時處理輸入資料
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go do(inCh, outCh, &wg)
}
go func() {
wg.Wait()
close(outCh)
}()
for r := range outCh {
fmt.Println(r)
}
}
func generator(n int) <-chan int {
outCh := make(chan int)
go func() {
for i := 0; i < n; i++ {
outCh <- i
}
close(outCh)
}()
return outCh
}
func do(inCh <-chan int, outCh chan<- int, wg *sync.WaitGroup) {
for v := range inCh {
outCh <- v * v
}
wg.Done()
}
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的奇淫巧技,說來看看?
- 如果這篇文章對你有幫助,請點個贊/喜歡,感謝。
- 本文作者:大彬
- 如果喜歡本文,隨意轉載,但請保留此原文連結:http://lessisbetter.site/2019/01/20/golang-channel-all-usage/