本文 AI 產出,尚未審核

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


簡介

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

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

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


核心概念

1. 為什麼要關閉通道?

  • 通知接收端:當生產者(producer)已不會再寫入資料時,關閉通道讓所有正在 for ... rangev, 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.OnceWaitGroup 等工具能協助避免常見的競爭與 deadlock 問題。
  • 在實務開發中,將 通道的生命週期 明確規劃(產生 → 使用 → 關閉),配合單向通道與錯誤傳遞模型,能寫出 安全、可維護 的併發程式。

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