本文 AI 產出,尚未審核
Golang - 通道與同步
主題:Context 包的使用(取消與超時)
簡介
在 Go 的併發程式設計中,context 套件是管理 工作生命週期、取消 與 超時 的核心工具。無論是 HTTP 伺服器、資料庫查詢、或是長時間跑的背景工作,都需要一套統一的機制,讓呼叫端能在適當的時機終止子任務,避免資源泄漏與不可預期的阻塞。
context 的設計理念是 傳遞(propagation)而非 全域(global)。透過將 Context 物件作為函式的第一個參數,我們可以在呼叫鏈上層層傳遞取消訊號、截止時間或其他請求範圍的值,讓程式碼保持 可測試、可組合 與 易維護。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整介紹如何在 Go 中正確使用 context 來達成 取消 與 超時 的需求,適合剛踏入併發領域的初學者,也能為中階開發者提供實務參考。
核心概念
1. Context 的三大功能
| 功能 | 說明 | 常見使用情境 |
|---|---|---|
| 取消 (Cancellation) | 透過 CancelFunc 讓所有子任務收到 Done() 信號,主動停止執行。 |
HTTP 請求被客戶端關閉、使用者點擊「取消」按鈕。 |
| 超時 (Timeout / Deadline) | 為 Context 設定一個絕對的截止時間或相對的超時時長,時間到自動觸發取消。 |
資料庫查詢、遠端 API 呼叫、長時間批次處理。 |
| 值傳遞 (Value) | 允許在 Context 中儲存鍵值對,供子程式讀取。 |
追蹤請求 ID、使用者認證資訊、日誌欄位。 |
注意:
Context不應被用來傳遞可變的業務資料,僅限於請求範圍的元資訊。
2. 建立與傳遞 Context
// 建立根 Context(背景 Context)
ctx := context.Background()
// 以 WithCancel 包裝,取得取消函式
ctx, cancel := context.WithCancel(ctx)
// 必須在適當時機呼叫 cancel()
defer cancel()
在大多數情況下,我們會在 最外層(例如 HTTP handler、main 函式)建立 Context,然後把它 作為第一個參數 傳遞給所有需要的函式或 goroutine。
3. 監聽取消訊號
select {
case <-ctx.Done():
// 收到取消或超時訊號
err := ctx.Err() // context.Canceled 或 context.DeadlineExceeded
log.Printf("任務被終止: %v", err)
return
default:
// 正常執行的程式碼
}
Done() 會回傳一個只讀的 channel,當 Context 被取消或超時時會被關閉。永遠 以 select 或 for 迴圈配合 Done() 來檢查,避免死等。
程式碼範例
以下提供 五個 常見且實用的範例,說明 Context 在不同情境下的使用方式。每段程式碼皆附上說明註解,方便讀者快速掌握要點。
範例 1️⃣ 基本取消(WithCancel)
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 建立可取消的 Context
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 確保資源釋放
// 啟動一個長時間執行的 goroutine
go func() {
for i := 0; i < 5; i++ {
// 每秒輸出一次訊息
fmt.Println("工作中:", i)
time.Sleep(time.Second)
}
fmt.Println("工作完成")
}()
// 2 秒後手動觸發取消
time.AfterFunc(2*time.Second, func() {
fmt.Println("主程式呼叫 cancel()")
cancel()
})
// 監聽取消訊號
<-ctx.Done()
fmt.Println("主程式結束:", ctx.Err())
}
重點
cancel()會關閉ctx.Done(),所有監聽此 channel 的 goroutine 都會立即收到訊號。- 使用
defer cancel()可以保證即使程式提前結束也會釋放資源。
範例 2️⃣ 超時控制(WithTimeout)
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 設定 3 秒的超時
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// 模擬一個需要 5 秒才能完成的工作
done := make(chan struct{})
go func() {
time.Sleep(5 * time.Second) // 工作過長
close(done)
}()
select {
case <-done:
fmt.Println("工作順利完成")
case <-ctx.Done():
// 超時或被取消
fmt.Println("工作被終止:", ctx.Err()) // context.DeadlineExceeded
}
}
說明
WithTimeout會自動在指定時間後呼叫cancel(),等同於WithDeadline(time.Now().Add(d))。ctx.Err()能分辨是 超時 (DeadlineExceeded) 還是 手動取消 (Canceled)。
範例 3️⃣ 期限控制(WithDeadline)
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 設定絕對截止時間(現在起 4 秒後)
deadline := time.Now().Add(4 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
// 執行一段可能被截止的工作
for i := 0; ; i++ {
select {
case <-ctx.Done():
fmt.Println("截止時間到:", ctx.Err()) // DeadlineExceeded
return
default:
fmt.Printf("處理第 %d 筆資料\n", i)
time.Sleep(1 * time.Second)
}
}
}
要點
WithDeadline允許使用 絕對時間,適合需要根據外部事件(如排程)設定截止點的情況。- 若 deadline 已經過去,返回的
Context會立即為 已取消 狀態。
範例 4️⃣ 在 HTTP 伺服器中使用 Context(自動取消)
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
// r.Context() 會自動在 client 斷線時取消
ctx := r.Context()
// 模擬一個需要 10 秒才能完成的外部呼叫
resultCh := make(chan string, 1)
go func() {
// 假設這裡是長時間的 I/O
time.Sleep(10 * time.Second)
resultCh <- "完成結果"
}()
select {
case <-ctx.Done():
// 客戶端已關閉連線或請求逾時
http.Error(w, "請求已取消", http.StatusRequestTimeout)
fmt.Println("HTTP 請求被取消:", ctx.Err())
case res := <-resultCh:
fmt.Fprintln(w, res)
}
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("伺服器啟動於 :8080")
http.ListenAndServe(":8080", nil)
}
說明
http.Request內建的Context會在 客戶端斷線、伺服器關閉 或 設定的 Timeout 時自動取消。- 在長時間 I/O 時,務必 監聽
ctx.Done(),避免不必要的資源占用。
範例 5️⃣ 透過 Context 傳遞請求 ID(Value)
package main
import (
"context"
"fmt"
"net/http"
)
type ctxKey string
const requestIDKey ctxKey = "requestID"
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 產生唯一的 request ID(簡化示範)
reqID := fmt.Sprintf("%d", time.Now().UnixNano())
// 把 request ID 放入 Context
ctx := context.WithValue(r.Context(), requestIDKey, reqID)
// 把新 Context 傳給下一個 handler
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func handler(w http.ResponseWriter, r *http.Request) {
// 從 Context 取出 request ID
if v := r.Context().Value(requestIDKey); v != nil {
fmt.Fprintf(w, "Request ID: %s\n", v.(string))
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
// 加入 middleware
http.ListenAndServe(":8080", loggingMiddleware(mux))
}
重點
Context的 Value 功能適合傳遞 只讀、跨層級 的元資料,如 trace ID、使用者資訊。- 使用自訂型別作為 key(如
ctxKey)可以避免 key 衝突。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 / 最佳實踐 |
|---|---|---|
忘記呼叫 cancel() |
產生 goroutine 泄漏、資源無法釋放 | 在建立 Context 後立即使用 defer cancel(),即使不需要手動取消也能保證釋放。 |
在 select 中遺漏 default |
可能造成 阻塞,導致程式無法回應取消訊號 | 若需要非阻塞檢查,務必加入 default 分支;若要阻塞等待結果,使用 select 搭配 ctx.Done()。 |
將大量業務資料放入 Context |
增加記憶體使用、破壞 Context 的語意 |
僅放入 輕量、只讀 的元資料(如 ID、token),業務資料應透過參數傳遞。 |
使用 time.Sleep 取代 select 監聽 ctx.Done() |
無法即時感知取消,造成不必要的等待 | 永遠使用 select { case <-ctx.Done(): … } 來等待或中斷長時間操作。 |
在多個 goroutine 中共用同一 CancelFunc |
可能在不適當的時機提前取消 | 為每個子任務建立 獨立 的 Context(WithCancel、WithTimeout),或使用 父子層級 的 Context 來控制。 |
其他最佳實踐
- Context 必須是第一個參數
func DoSomething(ctx context.Context, arg1 string) error { … } - 永遠不要在
Context之外自行建立 channel 來傳遞取消訊號,除非有特殊需求。 - 對於 I/O 操作(如 DB、HTTP),優先使用支援
Context的 API(如http.NewRequestWithContext、sql.DB.QueryContext)。 - 在測試中使用
context.WithCancel或context.WithTimeout,可以模擬取消與超時情境,提高測試覆蓋率。 - 保持
Context輕量:避免在Context中儲存大型 slice、map 或指向可變結構的指標。
實際應用場景
| 場景 | 為何需要 Context |
典型實作方式 |
|---|---|---|
| HTTP 伺服器 | 客戶端可能隨時斷線,需即時釋放資源 | 使用 r.Context()、http.Server 的 ReadTimeout/WriteTimeout |
| 微服務間 RPC | 呼叫鏈路可能因上游服務失效而必須取消下游請求 | 在每個 RPC 客戶端傳入 Context,如 grpc.CallOption 中的 grpc.WaitForReady |
| 資料庫批次匯入 | 大量寫入可能因外部條件(如維護)被迫中止 | db.ExecContext(ctx, ...) 搭配 WithTimeout |
| 背景工作(Worker) | 系統關機或部署需要平滑停止所有工作 | 主程式建立根 Context,在 os.Signal 捕獲後呼叫 cancel() |
| 定時任務 | 任務執行時間不可超過預設上限,避免卡住排程 | context.WithDeadline 設定明確截止時間 |
總結
context是 Go 併發程式設計的 基礎建設,提供 取消、超時 與 值傳遞 三大功能。- 正確的使用模式是:在最外層建立
Context(或使用r.Context()),把它作為第一個參數 傳遞下去,並在需要的地方 監聽Done()以及 檢查Err()。 - 常見的陷阱包括忘記
cancel()、在長時間阻塞時未監聽ctx.Done(),以及濫用Context儲存大量業務資料。透過 defer cancel()、select + Done()、以及 輕量的 Value,即可避免這些問題。 - 在實務上,無論是 HTTP 服務、微服務 RPC、資料庫操作或背景工作,都應該把
Context融入流程,讓系統在面對斷線、超時或關機時能 快速、可預測 地回收資源。
掌握 Context 的使用技巧,將讓你的 Go 程式在 可靠性、可維護性 以及 效能 上都有顯著提升。祝你寫程式愉快,開發出更健壯的併發應用!