select 是 Go 用來同時等待多個 channel 操作的控制結構。它的核心用途是讓一個 goroutine 同時處理資料輸入、取消訊號、timer/ticker 與 fallback 行為。

本章目標

學完本章後,你將能夠:

  1. 理解 select 的基本語法
  2. 同時等待多個 channel
  3. ctx.Done() 停止事件迴圈
  4. 用 ticker 建立定時工作
  5. 理解 default 的 non-blocking 行為

【觀察】select 同時等待多個 channel

select 的核心規則是:多個 case 中哪個 channel 先 ready,就執行哪個 case。以下範例同時等待 job 和取消訊號:

 1func worker(ctx context.Context, jobs <-chan Job) {
 2    for {
 3        select {
 4        case <-ctx.Done():
 5            return
 6        case job := <-jobs:
 7            handle(job)
 8        }
 9    }
10}

這個 worker 不需要先固定等待 jobs,也不需要用輪詢檢查 context。select 會同時等待兩者。

【判讀】select loop 是長期 goroutine 的生命週期中心

長期 goroutine 的核心規則是:事件迴圈必須同時處理工作來源與退出訊號。只讀工作 channel 而不讀 ctx.Done(),goroutine 可能無法按上層要求停止。

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

ok == false 表示 channel 已關閉。這讓 worker 在「上層取消」和「工作來源結束」兩種情境都能退出。

【策略】ticker case 要有 Stop

ticker 的核心規則是:建立 ticker 後要呼叫 Stop(),避免不再使用時仍保留 runtime 資源。

 1func cleanupLoop(ctx context.Context) {
 2    ticker := time.NewTicker(time.Minute)
 3    defer ticker.Stop()
 4
 5    for {
 6        select {
 7        case <-ctx.Done():
 8            return
 9        case <-ticker.C:
10            cleanup()
11        }
12    }
13}

這種模式常用於定期清理、同步、掃描或報表輸出。

【執行】default 建立 non-blocking select

default 的核心規則是:沒有任何 channel ready 時,立即執行 default。這會讓 select 不阻塞。

1select {
2case jobs <- job:
3    return nil
4default:
5    return ErrQueueFull
6}

這段程式嘗試把 job 放進 channel;如果 channel 不能立即接收,就回傳 ErrQueueFull。這適合保護呼叫端不要被背景佇列卡住。

不適合用 default 的情境是你其實需要等待結果:

1select {
2case result := <-results:
3    return result
4default:
5    return Result{} // 可能過早返回
6}

若結果是必要的,應該等待或設定 timeout,而不是直接 default。

timeout pattern

timeout 的核心規則是:需要等待但不能無限等待時,用 time.After 或 context timeout。

1select {
2case result := <-results:
3    return result, nil
4case <-time.After(2 * time.Second):
5    return Result{}, errors.New("timeout")
6}

在較大的系統中,通常更偏好 context.WithTimeout,讓 timeout 可以沿呼叫鏈傳遞。