context.Context 是 Go 用來傳遞取消訊號、逾時與 request-scoped 資訊的標準機制。它的核心用途是讓一串呼叫知道「這件工作是否應該停止」。

本章目標

學完本章後,你將能夠:

  1. 理解 context 的取消語義
  2. 使用 context.WithCancel
  3. 使用 context.WithTimeout
  4. 在 goroutine 和函式呼叫鏈中傳遞 context
  5. 避免把 context 當成一般資料容器

【觀察】context 表示工作生命週期

context 的核心規則是:被取消的 context 代表這件工作不應繼續進行。長時間工作應定期檢查 ctx.Done()

 1func Run(ctx context.Context) error {
 2    for {
 3        select {
 4        case <-ctx.Done():
 5            return ctx.Err()
 6        default:
 7            doOneStep()
 8        }
 9    }
10}

ctx.Done() 是一個 channel。當 context 被取消或逾時,這個 channel 會被關閉。

【判讀】取消是由上層傳給下層

context 的方向規則是:上層建立 context,下層接收 context;下層不應保存 context,也不應自行決定整個系統的生命週期。

1func main() {
2    ctx, cancel := context.WithCancel(context.Background())
3    defer cancel()
4
5    go worker(ctx)
6
7    waitForSignal()
8    cancel()
9}

context.Background() 是根 context。context.WithCancel 回傳子 context 和 cancel 函式。當 cancel() 被呼叫,所有使用該 context 的下層工作都會收到停止訊號。

【策略】逾時用 WithTimeout,主動停止用 WithCancel

context 建立方式的核心規則是:不知道何時停止但需要手動停止,用 WithCancel;有明確時間限制,用 WithTimeout

1ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
2defer cancel()
3
4if err := fetchData(ctx); err != nil {
5    return err
6}

下層函式應該接收 context:

 1func fetchData(ctx context.Context) error {
 2    req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
 3    if err != nil {
 4        return err
 5    }
 6
 7    resp, err := http.DefaultClient.Do(req)
 8    if err != nil {
 9        return err
10    }
11    defer resp.Body.Close()
12    return nil
13}

timeout 到達,HTTP request 會被取消。

【執行】讓背景 goroutine 有序退出

背景 goroutine 的核心規則是:啟動時接收 context,迴圈中用 select 同時等待工作與取消訊號。

 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            handleJob(job)
11        }
12    }
13}

這個 worker 有兩種退出路徑:

退出原因對應 case
上層取消<-ctx.Done()
job channel 關閉ok == false

這比讓 goroutine 無限跑更安全,也比較容易測試。

設計檢查

把 context 存進 struct

context 的生命週期屬於單次操作,不應長期存在 struct 裡。通常把 context 作為函式第一個參數:

1func (s *Service) Do(ctx context.Context, input Input) error

忘記呼叫 cancel

WithCancelWithTimeoutWithDeadline 回傳的 cancel 應該被呼叫,釋放相關資源:

1ctx, cancel := context.WithTimeout(parent, time.Second)
2defer cancel()

用 context 傳一般參數

context value 適合 request-scoped metadata,例如 request ID。一般業務參數應放在函式參數或 struct 裡。