Channel ownership 的核心規則是:能保證不再送出資料的一方,才有資格關閉 channel。建立 channel 的程式碼不一定是 owner;真正的 owner 是掌握 send lifecycle 的元件。

本章目標

學完本章後,你將能夠:

  1. 用 send lifecycle 判斷誰能 close channel
  2. 分辨 sender、receiver、coordinator 的責任
  3. 用 channel direction 表達能力限制
  4. 設計多 sender 的安全關閉流程
  5. 用 context 表達接收端提早停止,而不是關閉不屬於自己的 channel

【觀察】close channel 的核心風險是責任不清

Channel 關閉錯誤的核心問題是 ownership 沒定義清楚。接收端想停止讀取時關閉輸入 channel,多個 sender 中任一個 sender 自行 close,共用 channel 被外部任意 close,這些都可能造成 panic 或資料遺失。

責任不清的示例:

1func Consume(input chan Event) {
2    defer close(input)
3
4    for event := range input {
5        handle(event)
6    }
7}

這段程式的問題是 Consume 只是 receiver,卻關閉了 sender 還可能使用的 channel。只要上游晚一點送資料,就會出現 send on closed channel

【判讀】close 的語意是不再有新值

close(ch) 的核心語意是「這個 channel 不會再收到新值」。它不是取消 goroutine 的通用手段,也不是釋放記憶體的必要動作。

單一 sender 可以安全 close:

 1func Produce(items []string) <-chan string {
 2    out := make(chan string)
 3
 4    go func() {
 5        defer close(out)
 6        for _, item := range items {
 7            out <- item
 8        }
 9    }()
10
11    return out
12}

這個 goroutine 是唯一 sender,因此它能保證迴圈結束後不再送出。receiver 可以用 range 讀到 channel 關閉為止:

1for item := range Produce([]string{"a", "b"}) {
2    fmt.Println(item)
3}

receiver 不需要 close out。接收完資料是 receiver 的狀態,不代表 sender 的生命週期已經結束。

【策略】先畫出 sender 和 receiver

Channel 設計的核心動作是先列出誰會 send、誰會 receive、誰知道所有 sender 已經結束。這比先決定 buffer 大小更重要。

角色能力close 責任
single sender送出資料,知道自己何時結束擁有 close 責任
receiver接收資料,可能提早停止透過 context 通知停止
coordinator等待所有 sender 結束擁有 close 責任
external caller持有 channel reference 但不了解生命週期不參與 close 決策

如果無法回答「誰知道所有 sender 都結束」,就不應該 close 這個 channel。沒有 close 不一定是 bug;錯誤 close 才是更嚴重的問題。

【執行】多 sender 需要 coordinator 關閉

多個 goroutine 送往同一個 channel 時,關閉責任必須交給 coordinator。任一 sender 都不能單方面 close,因為其他 sender 可能還在送。

 1func Merge(inputs ...<-chan Event) <-chan Event {
 2    out := make(chan Event)
 3    var wg sync.WaitGroup
 4
 5    wg.Add(len(inputs))
 6    for _, input := range inputs {
 7        input := input
 8        go func() {
 9            defer wg.Done()
10            for event := range input {
11                out <- event
12            }
13        }()
14    }
15
16    go func() {
17        wg.Wait()
18        close(out)
19    }()
20
21    return out
22}

轉送 goroutine 只負責送資料。另一個 goroutine 等所有 sender 結束後才 close out。這把「送資料」和「宣告所有資料送完」分成兩個責任。

【執行】接收端提早停止要用 context

Receiver 提早停止的核心做法是通知上游停止,而不是關閉輸入 channel。context.Context 是 Go 服務中最常見的停止訊號。

 1func Consume(ctx context.Context, input <-chan Event) error {
 2    for {
 3        select {
 4        case <-ctx.Done():
 5            return ctx.Err()
 6        case event, ok := <-input:
 7            if !ok {
 8                return nil
 9            }
10            handle(event)
11        }
12    }
13}

Consume 可以因為 context 取消而退出,也可以因為 input 關閉而退出。它沒有 close input,因為 input 的 send lifecycle 不屬於它。

這個邊界在服務中很重要。HTTP handler、background worker、connection writer 都可能提早退出,但不能任意 close 上游仍可能使用的 channel。

【策略】channel direction 把能力寫進型別

Channel direction 的核心價值是限制函式能做的事。chan<- T 只能 send,<-chan T 只能 receive;這讓 ownership 更容易被讀者看見。

 1func StartWorker(ctx context.Context, jobs <-chan Job, results chan<- Result) {
 2    for {
 3        select {
 4        case <-ctx.Done():
 5            return
 6        case job, ok := <-jobs:
 7            if !ok {
 8                return
 9            }
10            results <- Process(job)
11        }
12    }
13}

StartWorker 只能從 jobs 收資料,只能往 results 送資料。它不能 close jobs,因為型別上就不是 sender;它是否能 close results 則要看它是不是唯一 sender。

方向限制不會自動解決所有權,但它能減少誤用,也讓 API 比註解更可靠。

【判讀】done channel 和 data channel 分開表達不同語意

停止訊號的核心語意應該和資料流分開。資料 channel 傳遞值;done channel 或 context 表示停止。把兩者混在一起會讓 close 的語意變模糊。

較清楚的設計:

 1type Worker struct {
 2    jobs <-chan Job
 3}
 4
 5func (w Worker) Run(ctx context.Context) {
 6    for {
 7        select {
 8        case <-ctx.Done():
 9            return
10        case job, ok := <-w.jobs:
11            if !ok {
12                return
13            }
14            process(job)
15        }
16    }
17}

jobs 關閉代表沒有更多 job。ctx.Done() 代表上層要求停止。這兩種退出原因不同,分開處理才能在 log、metric 或測試中看清楚。

【測試】測試 close 行為要避免靠 sleep

Channel ownership 的測試目標是確認 sender 結束後會 close、receiver 取消時不會 panic、多 sender 等全部完成才 close。這類測試應使用 channel 同步,不應依賴任意 sleep。

 1func TestProduceClosesOutput(t *testing.T) {
 2    output := Produce([]string{"a"})
 3
 4    if got := <-output; got != "a" {
 5        t.Fatalf("first value = %q, want %q", got, "a")
 6    }
 7
 8    _, ok := <-output
 9    if ok {
10        t.Fatalf("output should be closed after producer finishes")
11    }
12}

多 sender 測試可以讀到輸出 channel 關閉為止,確認所有值都收到:

 1func TestMergeClosesAfterAllInputsClose(t *testing.T) {
 2    a := make(chan Event, 1)
 3    b := make(chan Event, 1)
 4    a <- Event{ID: "a"}
 5    b <- Event{ID: "b"}
 6    close(a)
 7    close(b)
 8
 9    output := Merge(a, b)
10    got := map[string]bool{}
11    for event := range output {
12        got[event.ID] = true
13    }
14
15    if !got["a"] || !got["b"] {
16        t.Fatalf("merge should forward all events before closing")
17    }
18}

這個測試沒有固定等待時間。它把 channel close 本身當成同步訊號,結果更穩定。

本章不處理

本章先聚焦單一 Go process 內的 channel close 與 goroutine lifecycle;跨 process 的 ack、[consumer group](/go-advanced/backend/knowledge-cards/consumer-group) 與分散式訊號,會在下列章節再往外延伸:

和 Go 教材的關係

這一章承接的是 channel、goroutine 與 select 的協作;如果你要先回看語言教材,可以讀:

小結

Channel ownership 的核心是 send lifecycle。唯一 sender 可以在送完後 close;多 sender 需要 coordinator 統一 close;receiver 想停止時應使用 context,而不是關閉輸入 channel。把 sender、receiver、coordinator 分清楚,才能避免 send on closed channel、goroutine leak 與資料流混亂。