本文 AI 產出,尚未審核

Golang – 通道的關閉與迭代(close, range)


簡介

在 Go 語言的併發模型中,通道(channel)是連接不同 goroutine 的核心管道。除了基本的「傳送」與「接收」操作外,關閉通道與使用 range 迭代通道是兩個常被忽視卻相當重要的技巧。

  • 正確關閉通道可以讓接收端知道「已無更多資料」而安全退出迴圈,避免 goroutine 永遠阻塞。
  • range 讓我們以 for‑each 方式遍歷通道,程式碼更簡潔且天然支援多值接收的結束判斷。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹 close 與 range 的使用方式,幫助你在實務開發中寫出更安全、可讀的併發程式。


核心概念

1. 為什麼要關閉通道?

  • 通知接收端:當生產者(producer)已不會再寫入資料時,關閉通道讓所有正在 for ... range 或 v, ok := <-ch 的接收端立即得到 ok == false,可自行結束。
  • 資源釋放:關閉通道會釋放底層的緩衝區(如果有),減少記憶體佔用。
  • 避免寫入 panic:對已關閉的通道再寫入會觸發 runtime panic,這是一個明顯的錯誤訊號,能在開發階段快速定位問題。

注意:只有寫入端(producer)應該負責關閉通道;接收端 不應 主動關閉,除非有明確的協議。

2. close 的語法

close(ch)          // ch 必須是可寫的 channel
  • close 只能呼叫一次;重複關閉會 panic。
  • 關閉後仍可 接收,但不允許再 寫入。

3. 使用 range 迭代通道

for v := range ch { … } 會持續從通道讀取,直到通道被關閉且緩衝區耗盡。此語法自動處理 ok 判斷,讓程式碼更乾淨。

for v := range ch {
    fmt.Println(v)   // 只要還有值就會執行
}
fmt.Println("通道已關閉,迴圈結束")

4. ok 判斷的手寫版

有時需要同時取得值與是否成功的資訊,這時使用「多值接收」:

v, ok := <-ch
if !ok {
    // 通道已關閉且沒有資料可讀
}

程式碼範例

以下示範 5 個實用範例,涵蓋基本關閉、range、多值接收、緩衝通道與錯誤處理。

範例 1 – 基本的生產者/消費者,使用 close 結束

package main

import (
    "fmt"
    "time"
)

func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
        fmt.Printf("產生 %d\n", i)
        time.Sleep(200 * time.Millisecond)
    }
    close(ch) // 生產完畢,關閉通道
}

func consumer(ch <-chan int) {
    for v := range ch { // 直到通道關閉才會跳出
        fmt.Printf("消費 %d\n", v)
    }
    fmt.Println("所有資料已消費完畢")
}

func main() {
    ch := make(chan int)
    go producer(ch)
    consumer(ch)
}

說明:producer 在寫完 5 個整數後呼叫 close(ch),consumer 透過 range 自動偵測關閉並結束。


範例 2 – 多值接收 (ok) 與手動迴圈

package main

import "fmt"

func main() {
    ch := make(chan string, 3)
    ch <- "apple"
    ch <- "banana"
    close(ch) // 關閉前已寫入兩筆

    for {
        v, ok := <-ch
        if !ok { // 通道已關閉且緩衝區空
            fmt.Println("沒有更多資料")
            break
        }
        fmt.Println("收到:", v)
    }
}

說明:使用 v, ok := <-ch 可以在不使用 range 的情況下自行控制迴圈結束時機,適合需要在每次接收時執行額外邏輯的情境。


範例 3 – 緩衝通道的關閉與迭代

package main

import (
    "fmt"
    "sync"
)

func main() {
    const workers = 3
    jobs := make(chan int, 5) // 緩衝區大小 5

    // 產生工作
    go func() {
        for i := 1; i <= 8; i++ {
            jobs <- i
            fmt.Println("派發工作", i)
        }
        close(jobs) // 所有工作派發完畢,關閉通道
    }()

    var wg sync.WaitGroup
    wg.Add(workers)

    // 多個工作者同時消費
    for i := 0; i < workers; i++ {
        go func(id int) {
            defer wg.Done()
            for job := range jobs { // 會自動等到通道關閉
                fmt.Printf("工作者 %d 處理 %d\n", id, job)
            }
            fmt.Printf("工作者 %d 完成\n", id)
        }(i)
    }

    wg.Wait()
    fmt.Println("全部工作已完成")
}

說明:緩衝通道允許生產者在沒有即時消費者的情況下先寫入多筆資料;關閉後所有工作者會同時收到結束訊號。


範例 4 – 防止重複關閉的保護(使用 sync.Once)

package main

import (
    "fmt"
    "sync"
)

func main() {
    ch := make(chan int)
    var once sync.Once

    closeChannel := func() {
        once.Do(func() {
            close(ch)
            fmt.Println("通道已安全關閉")
        })
    }

    // 模擬多個 goroutine 可能同時想關閉通道
    for i := 0; i < 3; i++ {
        go closeChannel
    }

    // 讀取測試
    go func() {
        for v := range ch {
            fmt.Println("收到:", v)
        }
        fmt.Println("迴圈結束")
    }()

    // 稍等後結束程式
    fmt.Scanln()
}

說明:sync.Once 能保證 close(ch) 只會執行一次,避免因競爭條件產生 panic。


範例 5 – 產生者在遇到錯誤時關閉通道並傳遞錯誤

package main

import (
    "errors"
    "fmt"
)

type result struct {
    value int
    err   error
}

func producer(ch chan<- result) {
    for i := 0; i < 5; i++ {
        if i == 3 { // 模擬錯誤
            ch <- result{err: errors.New("發生錯誤")}
            close(ch)
            return
        }
        ch <- result{value: i}
    }
    close(ch) // 正常結束
}

func main() {
    ch := make(chan result)
    go producer(ch)

    for r := range ch {
        if r.err != nil {
            fmt.Println("收到錯誤:", r.err)
            break
        }
        fmt.Println("收到值:", r.value)
    }
    fmt.Println("結束")
}

說明:將錯誤包在結構體中傳遞,接收端在 range 迭代時即可偵測並提前退出。


常見陷阱與最佳實踐

陷阱 可能的結果 解決方式
在接收端關閉通道 產生 panic(close of closed channel)或導致寫入端阻塞 只讓寫入端負責關閉;若多個寫入端,使用 sync.Once 或協調機制
忘記關閉通道 接收端的 range 永遠不會結束,導致 goroutine 泄漏 在生產者結束前一定呼叫 close(ch),或使用 defer close(ch)
在 for range 內再次關閉通道 Panic range 迭代期間不應再次呼叫 close,除非有明確的保護 (sync.Once)
對已關閉的通道寫入 Panic 使用 select 搭配 default 或在寫入前檢查狀態,或改用 sync.WaitGroup 控制生命週期
使用無緩衝通道導致 deadlock 程式卡住 確保有相對應的接收者在寫入前就緒,或使用緩衝通道降低同步需求

最佳實踐

  1. 單向通道:在函式參數中使用 chan<-(只寫)或 <-chan(只讀)明確表達意圖,避免錯誤的關閉或寫入。
  2. defer close(ch):在產生者函式最前面使用 defer,確保即使中途返回也會關閉通道。
  3. 使用 sync.WaitGroup:配合通道關閉,確保所有消費者在主程式結束前已完成工作。
  4. 錯誤傳遞:將錯誤包在結構體或使用多個通道(資料 + 錯誤)傳遞,讓接收端能即時感知異常。
  5. 避免重複關閉:多寫入端時,使用 sync.Once 或「關閉訊號」通道(close(done))來協調。

實際應用場景

場景 為什麼需要 close / range 範例
工作隊列(Worker Pool) 生產者在所有工作排入後關閉通道,工作者透過 range 自動退出 範例 3
資料流管線(Pipeline) 每個階段都是一個 goroutine,前一階段結束後關閉通道,下一階段使用 range 讀取完畢 多階段資料處理
事件廣播(Pub/Sub) 發布者在不再產生事件時關閉通道,訂閱者使用 range 迭代直至結束 訊息推送系統
限速器(Rate Limiter) 使用緩衝通道作為 token bucket,關閉通道表示服務已關閉 API 限流
錯誤回報 產生者在遇到致命錯誤時立即關閉通道,同時把錯誤送出,接收端可立即終止 範例 5

總結

  • 關閉通道是 通知 接收端「資料已完結」的唯一正規方式,應由寫入端負責,且只能關閉一次。
  • 使用 range 迭代可以自動處理 ok 判斷,使程式碼更簡潔;若需要額外邏輯,仍可使用 v, ok := <-ch。
  • 緩衝通道、sync.Once、WaitGroup 等工具能協助避免常見的競爭與 deadlock 問題。
  • 在實務開發中,將 通道的生命週期 明確規劃(產生 → 使用 → 關閉),配合單向通道與錯誤傳遞模型,能寫出 安全、可維護 的併發程式。

掌握了 close 與 range 的正確用法,你就能在 Go 的併發世界裡,建立穩定且高效的資料流與工作流程。祝你寫程式愉快,期待看到你在專案中運用這些技巧!