總結了才知道,原來channel有這麼多用法!

大彬發表於2019-01-21

這篇文章總結了channel的10種常用操作,以一個更高的視角看待channel,會給大家帶來對channel更全面的認識。

在介紹10種操作前,先簡要介紹下channel的使用場景、基本操作和注意事項。

channel的使用場景

把channel用在資料流動的地方

  1. 訊息傳遞、訊息過濾
  2. 訊號廣播
  3. 事件訂閱與廣播
  4. 請求、響應轉發
  5. 任務分發
  6. 結果彙總
  7. 併發控制
  8. 同步與非同步
  9. ...

channel的基本操作和注意事項

channel存在3種狀態

  1. nil,未初始化的狀態,只進行了宣告,或者手動賦值為nil
  2. active,正常的channel,可讀或者可寫
  3. closed,已關閉,千萬不要誤認為關閉channel後,channel的值是nil

channel可進行3種操作

  1. 關閉

把這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. 為操作加上超時

  • 場景:需要超時控制的操作
  • 原理:使用selecttime.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的奇淫巧技,說來看看?

  1. 如果這篇文章對你有幫助,請點個贊/喜歡,感謝
  2. 本文作者:大彬
  3. 如果喜歡本文,隨意轉載,但請保留此原文連結:http://lessisbetter.site/2019/01/20/golang-channel-all-usage/

相關文章