目錄
- 什麼是 Context?
- Context 介面與基本概念
- Background 與 TODO
- WithCancel
- WithTimeout 與 WithDeadline
- WithValue 傳遞請求範圍資料
- 取消傳播機制
- 實戰範例
- 最佳實踐
- 常見問題
- 總結
- 參考資源
什麼是 Context?
context.Context 是 Go 標準庫的型別,用來在多 goroutine 之間傳遞取消訊號、deadline、與請求範圍 metadata。
為什麼需要 Context?
場景:HTTP server 收到一個 request,會啟動多個 goroutine(查 DB、呼外部 API、處理快取)。如果 client 在中途斷線,這些 goroutine 該怎麼知道要停?
沒有 Context 的方案:
- 自己定義 cancel channel 傳給每個 goroutine
- 各層自己實作 timeout
- 沒有統一機制 → 容易漏
有 Context 的方案:
- 標準介面,所有 goroutine 統一聽
ctx.Done() - 取消會自動傳播到所有衍生 context
- timeout / deadline 內建
核心特點
- 🎯 取消訊號傳播:父 context 取消 → 所有子 context 自動取消
- ⚡ Deadline / Timeout:到時間自動取消
- 🔧 請求範圍 Value:傳遞 trace ID、user info 等
- 📦 不可變:每個衍生函式都回傳新 context
- 🚀 慣例:函式第一個參數
ctx context.Context
Context 介面與基本概念
介面定義
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
每個方法的角色:
| 方法 | 用途 |
|---|---|
Done() |
回傳 channel,被關閉 = context 已被取消 |
Err() |
如果 Done 關閉了,回傳取消原因 |
Deadline() |
取得 deadline(如果有) |
Value(key) |
取出之前 WithValue 存的資料 |
「Done」channel 的用法
select {
case <-ctx.Done():
// context 被取消(外部 cancel、超時、deadline 到)
return ctx.Err()
case result := <-resultChan:
// 拿到結果
return result, nil
}
ctx.Done() 回傳的 channel 在以下情況會被關閉:
- 父 context 取消
- 自己被 cancel() 呼叫
- timeout / deadline 到
Err() 取消原因
ctx.Err() == nil // 還沒取消
ctx.Err() == context.Canceled // 手動 cancel
ctx.Err() == context.DeadlineExceeded // 超時
Background 與 TODO
兩個「根 context」:
context.Background() // 程式進入點用
context.TODO() // 暫時不知道用什麼時用
Background
- 程式最頂層用(
main函式、初始化、測試) - 永不取消、永無 deadline、無 value
- 所有其他 context 都是從 Background 衍生而來
func main() {
ctx := context.Background()
// 從 main 衍生有 timeout 的 context
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
runServer(ctx)
}
TODO
- 程式設計階段,「還沒決定該傳什麼 context」時用
- 行為跟 Background 完全相同
- 給 linter / reviewer 看的訊號:「這裡之後該想清楚」
func legacyCode() {
// TODO: 之後該從上層接 context
ctx := context.TODO()
callAPI(ctx)
}
何時用哪個:
| 情境 | 用 |
|---|---|
main、init、頂層 |
Background |
| 測試(沒有自然上游 ctx) | Background |
| 重構中,先放著 | TODO |
WithCancel
context.WithCancel(parent) 回傳新 context + 一個 cancel function。呼叫 cancel 會關閉 Done channel。
基本用法
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 永遠記得 defer cancel
go worker(ctx)
time.Sleep(time.Second)
cancel() // 主動取消
Worker 端聽訊號
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("worker 收到取消訊號,原因:", ctx.Err())
return
default:
// 做事
time.Sleep(100 * time.Millisecond)
}
}
}
為什麼一定要 cancel?
即使 context 因為 timeout 自動取消,也必須呼叫 cancel:
ctx, cancel := context.WithCancel(parent)
defer cancel() // 必須
// 否則 context 內部資源不會釋放,造成 leak
go vet 會幫你檢查這點:
$ go vet ./...
the cancel function returned by context.WithCancel should be called, not discarded
WithTimeout 與 WithDeadline
兩個都會自動取消,差別只是表達方式。
WithTimeout
// 從現在起 5 秒後自動取消
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
select {
case <-time.After(10 * time.Second):
fmt.Println("不會印(會先被取消)")
case <-ctx.Done():
fmt.Println("取消:", ctx.Err()) // context deadline exceeded
}
WithDeadline
// 指定絕對時間
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(parent, deadline)
defer cancel()
WithTimeout(parent, t) 等於 WithDeadline(parent, time.Now().Add(t))。
Deadline 繼承父 context
子 context 的 deadline 不能比父晚:
parent, cancelP := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelP()
// 想設 10 秒?實際還是 5 秒(取父子最小值)
child, cancelC := context.WithTimeout(parent, 10*time.Second)
defer cancelC()
d, _ := child.Deadline()
fmt.Println("實際 deadline:", time.Until(d)) // ~5s
WithValue 傳遞請求範圍資料
傳遞「跟整個 request 相關」的資料,例如 trace ID、user info。
基本用法
type ctxKey string
const userIDKey ctxKey = "userID"
func setUserID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, userIDKey, id)
}
func getUserID(ctx context.Context) (string, bool) {
id, ok := ctx.Value(userIDKey).(string)
return id, ok
}
// 使用
ctx := context.Background()
ctx = setUserID(ctx, "user-123")
if id, ok := getUserID(ctx); ok {
fmt.Println("UserID:", id)
}
Key 必須是自訂型別
// ❌ 用 string 當 key 容易跟其他套件衝突
ctx = context.WithValue(ctx, "userID", "123")
// ✅ 用自訂型別避免衝突
type ctxKey int
const userIDKey ctxKey = iota
ctx = context.WithValue(ctx, userIDKey, "123")
規則:key 用未匯出的自訂型別,這樣其他套件不可能放同一個 key。
包裝 getter / setter
package mycontext
type ctxKey int
const userKey ctxKey = iota
func WithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
func UserFrom(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(userKey).(*User)
return u, ok
}
呼叫端用:
ctx = mycontext.WithUser(ctx, u)
u, _ := mycontext.UserFrom(ctx)
WithValue 該存什麼?該不該存?
該存:
- ✅ Request ID / Trace ID
- ✅ User ID / Session token
- ✅ Logger(with request context)
- ✅ Locale / timezone
不該存:
- ❌ DB 連線
- ❌ 設定參數(用 function argument)
- ❌ 函式行為控制 flag(function argument 更明確)
判斷:「這個資料是不是請求範圍、只在跨函式呼叫鏈傳遞用?」如果是,OK。否則用 function argument。
取消傳播機制
Context 的最大價值:取消會自動沿著樹往下傳。
父取消 → 子全部取消
parent, cancelP := context.WithCancel(context.Background())
child1, _ := context.WithCancel(parent)
child2, _ := context.WithCancel(parent)
grandchild, _ := context.WithCancel(child1)
cancelP() // 取消 parent
// child1.Done()、child2.Done()、grandchild.Done() 都會關閉
圖示
Background
│
WithCancel ← cancelP() 在這邊
│
parent ctx
/ │ \
child1 child2
│
grandchild
cancelP() → 整棵子樹都收到 Done 訊號
子取消不影響父
parent, _ := context.WithCancel(context.Background())
child, cancelC := context.WithCancel(parent)
cancelC() // 只取消 child
// parent.Done() 還沒關
// child.Done() 已關
Worker pool 場景
func processItems(ctx context.Context, items []string) error {
var wg sync.WaitGroup
errCh := make(chan error, len(items))
// 派 n 個 worker
for _, item := range items {
wg.Add(1)
go func(it string) {
defer wg.Done()
select {
case <-ctx.Done():
errCh <- ctx.Err()
return
default:
}
if err := processOne(ctx, it); err != nil {
errCh <- err
}
}(item)
}
wg.Wait()
close(errCh)
for err := range errCh {
if err != nil {
return err // 一個錯就 return(其他 worker 還跑,可選擇 cancel)
}
}
return nil
}
更好的做法用 errgroup:
import "golang.org/x/sync/errgroup"
func processItems(ctx context.Context, items []string) error {
g, gctx := errgroup.WithContext(ctx)
for _, item := range items {
item := item
g.Go(func() error {
return processOne(gctx, item)
})
}
return g.Wait() // 任一 goroutine 出錯 → cancel gctx → 其他全停
}
實戰範例
範例 1:HTTP server 全鏈路 context
package main
import (
"context"
"database/sql"
"fmt"
"net/http"
"time"
)
func main() {
http.HandleFunc("/user/", handleUser)
http.ListenAndServe(":8080", nil)
}
func handleUser(w http.ResponseWriter, r *http.Request) {
// r.Context() 在 client 斷線時會自動取消
ctx := r.Context()
// 加 5 秒上限
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
id := r.URL.Path[len("/user/"):]
user, err := getUser(ctx, id)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
http.Error(w, "timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "%+v", user)
}
func getUser(ctx context.Context, id string) (User, error) {
db := getDB()
// database/sql 支援 context,會在取消時取消查詢
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id)
var u User
if err := row.Scan(&u.Name); err != nil {
return User{}, err
}
return u, nil
}
type User struct {
Name string
}
func getDB() *sql.DB { return nil } // 略
範例 2:取消可能慢的計算
func slowComputation(ctx context.Context) (int, error) {
resultCh := make(chan int, 1)
go func() {
// 慢慢算
time.Sleep(5 * time.Second)
resultCh <- 42
}()
select {
case r := <-resultCh:
return r, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
r, err := slowComputation(ctx)
if err != nil {
fmt.Println("失敗:", err) // context deadline exceeded
return
}
fmt.Println(r)
}
⚠️ 注意:上面的 goroutine 還會繼續跑(無法強制終止),只是不再 wait 結果。要真的避免浪費需在計算內檢查 ctx.Done()。
範例 3:定期檢查取消的長運算
func chunkedWork(ctx context.Context, items []Item) error {
for i, item := range items {
// 每處理完一個檢查一次
select {
case <-ctx.Done():
return fmt.Errorf("處理到第 %d 個被取消: %w", i, ctx.Err())
default:
}
if err := process(item); err != nil {
return err
}
}
return nil
}
範例 4:自訂 logger 注入 context
package mylog
import "context"
type loggerCtxKey struct{}
type Logger struct {
RequestID string
}
func (l *Logger) Info(msg string) {
fmt.Printf("[%s] %s\n", l.RequestID, msg)
}
func WithLogger(ctx context.Context, l *Logger) context.Context {
return context.WithValue(ctx, loggerCtxKey{}, l)
}
func From(ctx context.Context) *Logger {
if l, ok := ctx.Value(loggerCtxKey{}).(*Logger); ok {
return l
}
return &Logger{RequestID: "unknown"}
}
使用:
ctx := mylog.WithLogger(r.Context(), &mylog.Logger{RequestID: uuid.New().String()})
processRequest(ctx)
// 子函式
func processRequest(ctx context.Context) {
log := mylog.From(ctx)
log.Info("processing")
}
最佳實踐
1. 第一個參數放 ctx,永遠
// ✅
func GetUser(ctx context.Context, id string) (*User, error)
func ProcessBatch(ctx context.Context, items []Item) error
// ❌ 不要藏在結構或第二個參數
func GetUser(id string, ctx context.Context) (*User, error)
func (s *Service) ContextField *Context // 別把 ctx 存進 struct
2. 永遠 defer cancel
// ✅
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()
// ❌ 漏 cancel → 資源 leak
ctx, _ := context.WithTimeout(parent, 5*time.Second)
3. 不要把 ctx 存進 struct
// ❌ struct 不該存 ctx
type Worker struct {
ctx context.Context // 反模式
}
// ✅ 每個 method 接 ctx
type Worker struct {}
func (w *Worker) Do(ctx context.Context) error { ... }
理由:ctx 是請求範圍的,存進長壽 struct 會跟著 struct 活很久,違反設計初衷。
4. Value key 用未匯出型別
// ❌ 容易衝突
ctx = context.WithValue(ctx, "user", u)
// ✅ 套件私有 key
type ctxKey int
const userKey ctxKey = iota
ctx = context.WithValue(ctx, userKey, u)
5. 寫支援 context 的函式時,立即檢查
func doWork(ctx context.Context) error {
// 一進來就檢查
if err := ctx.Err(); err != nil {
return err
}
// ...慢操作
}
6. nil ctx 永遠不要傳
// ❌
doWork(nil)
// ✅ 沒上游 ctx 就用 Background
doWork(context.Background())
go vet 會檢查這點。
7. 不要用 context.Value 當 function argument
// ❌ 用 ctx 偷渡參數,難維護
func handle(ctx context.Context) {
timeout := ctx.Value("timeout").(time.Duration)
// ...
}
// ✅ 直接當參數
func handle(ctx context.Context, timeout time.Duration) {
// ...
}
只用 ctx.Value 傳「跟整個 request lifecycle 相關」的資料(trace ID、user info)。
常見問題
問題 1:取消 ctx 後子函式還在跑
原因:子函式沒檢查 ctx.Done()
解決:
- 慢迴圈內每 N 次檢查一次
- 用
<-ctx.Done()配合 select
for i := range items {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
process(items[i])
}
問題 2:goroutine leak
症狀:函式 return 了,但內部 goroutine 還在跑
// ❌ 沒 cancel
func leaky() {
ctx, _ := context.WithTimeout(context.Background(), time.Hour)
go func() {
<-ctx.Done()
fmt.Println("結束")
}()
// 函式 return 但 goroutine 等 1 小時才會跑完
}
解決:永遠 defer cancel
問題 3:WithValue 偷渡函式參數
反模式:
ctx = context.WithValue(ctx, "limit", 100)
ctx = context.WithValue(ctx, "offset", 0)
listUsers(ctx) // 內部讀 ctx 拿 limit/offset
解決:直接當參數
listUsers(ctx, 100, 0)
問題 4:deadline 沒生效
檢查:
- 函式內部有沒有用到 ctx?(database/sql、net/http 等大多支援)
- 是不是建了新 context 沒從父繼承?
// ❌ 沒繼承父
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// 應該從父 context 來
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
問題 5:ctx 跨 goroutine 但取消傳不過去
// ❌ 開新 goroutine 沒傳 ctx
go func() {
doSlowThing() // 沒接 ctx,外部取消不到
}()
// ✅
go func() {
doSlowThing(ctx) // 傳進去
}()
問題 6:context.Canceled vs context.DeadlineExceeded
兩個都讓 Done() 關閉,但 Err() 不同:
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
<-ctx.Done()
switch ctx.Err() {
case context.Canceled:
fmt.Println("手動 cancel")
case context.DeadlineExceeded:
fmt.Println("超時")
}
對 HTTP 場景常用:
Canceled→ client 斷線(回 499 / 不回應)DeadlineExceeded→ 超時(回 504 Gateway Timeout)
總結
核心要點
Context = 取消訊號 + Deadline + Value 三合一介面
慣例:函式第一個參數 ctx context.Context
取消會自動沿樹傳播
永遠 defer cancel(即使知道會超時自動取消)
設計原則:
- ✅ 第一個參數 ctx,不存進 struct
- ✅ 永遠
defer cancel() - ✅ WithValue 只放請求範圍 metadata
- ✅ Key 用未匯出自訂型別
- ✅ 長運算內定期檢查
ctx.Done()
速查表
// 根 context
context.Background() // main、init、test
context.TODO() // 暫不知道
// 衍生
ctx, cancel := context.WithCancel(parent)
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
ctx = context.WithValue(parent, key, val)
// 檢查
<-ctx.Done() // 取消訊號
ctx.Err() // 取消原因
ctx.Deadline() // 有沒有 deadline
v := ctx.Value(k) // 取值
// 永遠
defer cancel()
取消原因對照
| ctx.Err() | 意義 | HTTP 對應 |
|---|---|---|
nil |
還沒取消 | — |
context.Canceled |
手動 cancel / client 斷線 | 499 / 不回應 |
context.DeadlineExceeded |
超時 | 504 |
相關閱讀
- Go Goroutines 完全指南 — Context 跨 goroutine 取消的應用場景
- Go sync 同步原語 — Context 跟 WaitGroup / errgroup 搭配
- Go 錯誤處理 —
context.Canceled與DeadlineExceeded都是 error - Go 標準庫實戰 — net/http、database/sql 內建支援 context
參考資源
- 官方 context 套件:https://pkg.go.dev/context
- Go Concurrency Patterns: Context:https://go.dev/blog/context
- errgroup:https://pkg.go.dev/golang.org/x/sync/errgroup
- Sameer Ajmani - Context Best Practices:https://go.dev/talks/2014/gotham-context.slide
建立日期:2026-05-16 最後更新:2026-05-16