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

這篇文章總結了channel的10種經常使用操做,以一個更高的視角看待channel,會給你們帶來對channel更全面的認識。golang

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

channel的使用場景

把channel用在數據流動的地方併發

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

channel的基本操做和注意事項

channel存在3種狀態less

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

channel可進行3種操做異步

  1. 關閉

把這3種操做和3種channel狀態能夠組合出9種狀況高併發

操做 nil的channel 正常channel 已關閉channel
<- ch 阻塞 成功或阻塞 讀到零值
ch <- 阻塞 成功或阻塞 panic
close(ch) panic 成功 panic

對於nil通道的狀況,也並不是徹底遵循上表,有1個特殊場景:當nil的通道在select的某個case中時,這個case會阻塞,但不會形成死鎖。oop

參考代碼請看:https://dave.cheney.net/2014/...spa

下面介紹使用channel的10種經常使用操做。.net

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

11. 使用channel傳遞channel

  • 場景:使用場景有點多,一般是用來獲取結果。
  • 原理:channel能夠用來傳遞變量,channel自身也是變量,能夠傳遞本身。
  • 用法:下面示例展現了有序展現請求的結果,另外一個示例能夠見另外文章的版本3
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func main() {
    reqs := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}

    // 存放結果的channel的channel
    outs := make(chan chan int, len(reqs))
    var wg sync.WaitGroup
    wg.Add(len(reqs))
    for _, x := range reqs {
        o := handle(&wg, x)
        outs <- o
    }

    go func() {
        wg.Wait()
        close(outs)
    }()

    // 讀取結果,結果有序
    for o := range outs {
        fmt.Println(<-o)
    }
}

// handle 處理請求,耗時隨機模擬
func handle(wg *sync.WaitGroup, a int) chan int {
    out := make(chan int)
    go func() {
        time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
        out <- a
        wg.Done()
    }()
    return out
}

你有哪些channel的奇淫巧技,說來看看?

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

相關文章
相關標籤/搜索