15 分鐘學習 Go 語言如何處理多個 Channel 通道

golang logo

大家在初學 Go 語言時,肯定很少用到 Go Channel,也不太確定使用的時機點,其實在官方 Blog 有提供一篇不錯的文章『Go Concurrency Patterns: Pipelines and cancellation』,相信大家剛跨入學習新語言時,不會馬上看 Go Channel,底下我來用一個簡單的例子來說明如何使用 Go Channel,使用情境非常簡單,就是假設今天要同時處理 20 個背景工作,一定想到要使用 Goroutines,但是又想要收到這 20 個 JOB 處理的結果,並顯示在畫面上,如果其中一個 Job 失敗,就跳出 main 函式,當然又會希望這 20 個 JOB 預期在一分鐘內執行結束,如果超過一分鐘,也是一樣跳出 main 函式。針對這個問題,我們可以整理需要三個 Channel + 一個 Timeout 機制。

  • 使用 outChan 顯示各個 JOB 完成狀況
  • 使用 errChan 顯示 JOB 發生錯誤並且跳出 main 主程式
  • 使用 finishChan 通知全部 JOB 已經完成
  • 設定 Timeout 機制 (1 秒之內要完成所有 job)

在看此文章之前,也許可以先理解什麼是『buffer vs unbuffer channel』。

教學影片

更多實戰影片可以參考我的 Udemy 教學系列

實戰範例

針對上述的問題,先透過 Sync 套件的 WaitGroup 來確保 20 個 JOB 處理完成後才結束 main 函式。

package main

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

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(val int, wg *sync.WaitGroup) {
            time.Sleep(time.Duration(rand.Int31n(1000)) * time.Millisecond)
            fmt.Println("finished job id:", val)
            wg.Done()
        }(i, &wg)
    }

    wg.Wait()
}

大家可以先拿上面的範例來練習看看如何達到需求,而不是在 go func 內直接印出結果。

處理多個 Channel 通道

首先在 main 宣告三個 Channel 通道

    outChan := make(chan int)
    errChan := make(chan error)
    finishChan := make(chan struct{})

接著要在最後直接讀取這三個 Channel 值,可以透過 Select,由於 outChan 會傳入 20 個值,所以需要搭配 for 迴圈方式來讀取多個值

Loop:
    for {
        select {
        case val := <-outChan:
            fmt.Println("finished:", val)
        case err := <-errChan:
            fmt.Println("error:", err)
            break Loop
        case <-finishChan:
            break Loop
        }
    }

這邊我們看到需要加上 Loop 自定義 Tag,來達到 break for 迴圈,而不是 break select 函式。但是有沒有發現程式碼會一直卡在 wg.Wait(),不會進入到 for 迴圈內,這時候就必須將 wg.Wait() 丟到背景。

    go func() {
        wg.Wait()
        fmt.Println("finish all job")
        close(finishChan)
    }()

也就是當 20 個 job 都完成後,會觸發 close(finishChan),就可以在 for 迴圈內結束整個 main 函式。最後需要設定 timout 機制,請把 select 多補上一個 time.After()

Loop:
    for {
        select {
        case val := <-outChan:
            fmt.Println("finished:", val)
        case err := <-errChan:
            fmt.Println("error:", err)
            break Loop
        case <-finishChan:
            break Loop
        case <-time.After(100000 * time.Millisecond):
            break Loop
        }
    }

來看看 go func 內怎麼將值丟到 Channel

    for i := 0; i < 20; i++ {
        go func(outChan chan<- int, errChan chan<- error, val int, wg *sync.WaitGroup) {
            defer wg.Done()
            time.Sleep(time.Duration(rand.Int31n(1000)) * time.Millisecond)
            fmt.Println("finished job id:", val)
            outChan <- val
            if val == 11 {
                errChan <- errors.New("error in 60")
            }

        }(outChan, errChan, i, &wg)
    }

宣告 chan<- int 代表在 go func 只能將訊息丟到通道內,而不能讀取通道。

心得

希望透過上述簡單的例子,讓大家初學 Go 的時候有個基礎的理解。用法其實不難,但是請參考專案內容特性來決定如何使用 Channel,最後附上完整的程式碼:

package main

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

func main() {
    outChan := make(chan int)
    errChan := make(chan error)
    finishChan := make(chan struct{})
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go func(outChan chan<- int, errChan chan<- error, val int, wg *sync.WaitGroup) {
            defer wg.Done()
            time.Sleep(time.Duration(rand.Int31n(1000)) * time.Millisecond)
            fmt.Println("finished job id:", val)
            outChan <- val
            if val == 60 {
                errChan <- errors.New("error in 60")
            }

        }(outChan, errChan, i, &wg)
    }

    go func() {
        wg.Wait()
        fmt.Println("finish all job")
        close(finishChan)
    }()

Loop:
    for {
        select {
        case val := <-outChan:
            fmt.Println("finished:", val)
        case err := <-errChan:
            fmt.Println("error:", err)
            break Loop
        case <-finishChan:
            break Loop
        case <-time.After(100000 * time.Millisecond):
            break Loop
        }
    }
}

也可以在 Go Playground 試試看

  • Chien Wei Huang

    Loop:
    for {
    select {
    case val := <-outChan:
    fmt.Println("finished:", val)
    case err := <-errChan:
    fmt.Println("error:", err)
    break Loop
    case <-finishChan:
    break Loop
    case <-time.After(100000 * time.Millisecond):
    break Loop
    }
    }
    }

    會不會有問題啊
    因為 select channel 是 random choose 的

  • appleboy48

    不會三個同時出現吧

  • Chien Wei Huang

    可是 outChan 跟 errChan 是有可能會一起出現的吧?
    那就會出現即使 errChan 有訊號,但是 random choose 還是選了 outChan 走

  • appleboy48

    有實際的 case (或是範例) 嗎?因為這機制我用在寫 ssh command,目前沒有出現 errChan 跟 outChan 同時出現過。