context 包用于在 goroutine 之间传递取消信号、超时信号以及请求范围内的数据。
Context 接口回顾 所有 context 都实现同一个接口:
type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key any) any }
要点:
Done() 返回的 channel 只会被 close ,不会发送值。
Err() 只有在 Done() 关闭后才有意义,可能的值是 context.Canceled 或 context.DeadlineExceeded。
Context 是并发安全 的,可以被多个 goroutine 同时读取。
context.Background 作用 返回一个非 nil 的空 Context ,永不取消、没有 deadline、没有 value。它是所有 context 树的根节点 。
用法 ctx := context.Background()
典型场景
main 函数入口
初始化、测试代码
作为其他 context 的父节点
当你不确定该用什么 context 时的”安全默认值”
示例 package mainimport ( "context" "fmt" ) func main () { ctx := context.Background() fmt.Printf("%#v\n" , ctx) fmt.Println(ctx.Done()) fmt.Println(ctx.Err()) fmt.Println(ctx.Value("any" )) }
注意 :Background() 返回的 ctx 的 Done() 是 nil,所以 select { case <-ctx.Done(): } 会永远阻塞。它本身不能被取消。
context.TODO 作用 同样返回一个非 nil 的空 Context ,行为上和 Background() 完全等价 (底层都是 emptyCtx),区别只在于语义 :
Background:明确要把它当作根 context 。
TODO:当前还不确定 应该用什么 context,先占个位,将来会替换。
使用建议
在重构中 、还未确定上下文 的地方用 TODO()。
静态检查工具(如 staticcheck 的 SA1012)能识别出用 TODO 的地方,便于日后补全。
不要用 nil 作为 context ,需要占位时用 TODO()。
示例 func placeholder () { ctx := context.TODO() _ = ctx }
与 Background 的区别表
维度
Background
TODO
底层类型
emptyCtx
emptyCtx
可取消
否
否
有 deadline
否
否
有 value
否
否
语义
根节点
待定占位
生产代码推荐
用于根
尽量避免
context.WithCancel 作用 基于父 context 派生一个可手动取消 的子 context,返回 (ctx, cancel)。调用 cancel() 会:
关闭该 ctx 的 Done() channel;
让所有子 ctx 也一起被取消(取消具有向下传播 的特性);
释放内部资源。
函数签名 func WithCancel (parent Context) (ctx Context, cancel CancelFunc)
关键要点
必须调用 cancel (哪怕提前结束),否则会内存泄漏 (父 ctx 一直持有对子 ctx 的引用,直到父被取消)。go vet 的 lostcancel 检查会捕捉这个问题。
常见写法:defer cancel()。
典型场景
用户主动中止操作(比如 HTTP handler 的请求结束)。
一个任务失败后,取消其他并发任务 (errgroup 的核心机制)。
手动停止后台 goroutine。
事件监听、循环中退出。
手动取消后台任务 package mainimport ( "context" "fmt" "time" ) func main () { ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func () { for { select { case <-ctx.Done(): fmt.Println("worker 收到取消信号:" , ctx.Err()) return case <-time.After(500 * time.Millisecond): fmt.Println("worker 正在工作..." ) } } }() time.Sleep(2 * time.Second) cancel() time.Sleep(100 * time.Millisecond) }
取消向下传播 func propagate () { parent, cancelParent := context.WithCancel(context.Background()) defer cancelParent() child, cancelChild := context.WithCancel(parent) defer cancelChild() cancelParent() <-child.Done() fmt.Println("child 也被取消:" , child.Err()) }
并发任务中一个出错,取消其余任务 func raceTasks () error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() errCh := make (chan error, 2 ) tasks := []func (context.Context) error {taskA, taskB} for _, task := range tasks { go func (f func (context.Context) error ) { if err := f(ctx); err != nil { errCh <- err cancel() } }(task) } select { case err := <-errCh: return err case <-ctx.Done(): return ctx.Err() } } func taskA (ctx context.Context) error { select { case <-time.After(100 * time.Millisecond): return errors.New("taskA failed" ) case <-ctx.Done(): return ctx.Err() } } func taskB (ctx context.Context) error { select { case <-time.After(5 * time.Second): return nil case <-ctx.Done(): return ctx.Err() } }
context.WithTimeout 作用 基于父 context 派生一个在指定时间后自动取消 的子 context。返回 (ctx, cancel)。
函数签名 func WithTimeout (parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)
底层关系 WithTimeout(parent, d) == WithDeadline(parent, time.Now().Add(d))
关键要点
timeout 是相对时间 (从现在开始算多久后取消),容易阅读和修改。
同样必须调用 cancel :即使超时自动取消,cancel 也要调用来释放 timer 资源。
超时后 ctx.Err() 返回 context.DeadlineExceeded。
典型场景
HTTP 客户端调用 外部服务设置最大等待时间。
数据库查询 超时。
单次任务 的耗时上限。
用户请求 的服务端超时控制(例如 API 网关必须 3s 内返回)。
模拟一次带超时的 HTTP 调用 func fetchData (ctx context.Context, url string ) (string , error) { select { case <-time.After(3 * time.Second): return "data from " + url, nil case <-ctx.Done(): return "" , ctx.Err() } } func main () { ctx, cancel := context.WithTimeout(context.Background(), 1 *time.Second) defer cancel() start := time.Now() data, err := fetchData(ctx, "https://example.com/api" ) fmt.Printf("耗时: %v, data: %q, err: %v\n" , time.Since(start), data, err) }
context.WithDeadline 作用 与 WithTimeout 类似,但接收的是一个绝对时间点 time.Time,到该时间点时自动取消。
函数签名 func WithDeadline (parent Context, d time.Time) (ctx Context, cancel CancelFunc)
与 WithTimeout 的差别
特性
WithTimeout
WithDeadline
参数
相对时长 time.Duration
绝对时间 time.Time
可读性
简单直接,如 3*time.Second
需要先算好截止时刻
链式传播
父 deadline 更早时以父为准
同左
适用
单次操作的时长限制
多个操作共享同一个截止时刻,如”必须在 12:00 之前处理完”
重要 :如果 父 ctx 已有的 deadline 比传入的更早 ,那么新 ctx 会继承父的更早 deadline ,不会因为传入了更晚的时间而延迟取消。
典型场景
一次会话/批处理的整体截止时刻(多个子请求共享这个 deadline)。
多步流程 (如 3 步依次调用下游),希望整体不超过某时间点。
多步骤流程共享截止时间 func step1 (ctx context.Context) error { select { case <-time.After(400 * time.Millisecond): fmt.Println("step1 完成" ) return nil case <-ctx.Done(): return ctx.Err() } } func step2 (ctx context.Context) error { select { case <-time.After(2 * time.Second): fmt.Println("step2 完成" ) return nil case <-ctx.Done(): return ctx.Err() } } func main () { deadline := time.Now().Add(1 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel() if err := step1(ctx); err != nil { fmt.Println("step1 失败:" , err) return } if err := step2(ctx); err != nil { fmt.Println("step2 失败:" , err) return } }
注意:父 deadline 更早时优先 p, cancelP := context.WithTimeout(context.Background(), 1 *time.Second) defer cancelP()c, cancelC := context.WithDeadline(p, time.Now().Add(5 *time.Second)) defer cancelC()<-c.Done() fmt.Println(c.Err())
context.WithValue 作用 在 context 链上传递请求范围内的元数据 ,例如 traceID、用户 ID、认证 token、语言偏好等。
函数签名 func WithValue (parent Context, key, val any) Context
关键要点
key 必须是可比较的(comparable) ,否则运行时 panic。
不要用内置类型(如 string)做 key ——不同包之间的 "userID" 会冲突。推荐使用自定义的未导出类型 。
只用于传递”请求作用域的元数据” ,不要 用来传递函数参数、业务可选参数。
Value 查找是链式 向上查找,线性开销 ,不要滥用。
key 不能为 nil,否则 panic。
典型场景
分布式追踪 traceID / spanID
用户认证信息(注意安全性)
请求 ID、语言、租户 ID
中间件向下游函数传递元信息(如 HTTP middleware → handler → service)
不适用场景
传递业务参数(应该用函数入参)
存储大量数据或可变数据
存储可选配置
用自定义类型作为 key package mainimport ( "context" "fmt" ) type ctxKey int const ( traceIDKey ctxKey = iota userIDKey ) func withTrace (ctx context.Context, id string ) context .Context { return context.WithValue(ctx, traceIDKey, id) } func handle (ctx context.Context) { if v := ctx.Value(traceIDKey); v != nil { fmt.Println("traceID =" , v.(string )) } if v := ctx.Value(userIDKey); v != nil { fmt.Println("userID =" , v.(int )) } } func main () { ctx := withTrace(context.Background(), "trace-abc-123" ) ctx = context.WithValue(ctx, userIDKey, 42 ) handle(ctx) }
中间件传递 traceID func TraceMiddleware (next http.Handler) http .Handler { return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { traceID := r.Header.Get("X-Trace-ID" ) if traceID == "" { traceID = uuid.NewString() } ctx := context.WithValue(r.Context(), traceIDKey{}, traceID) next.ServeHTTP(w, r.WithContext(ctx)) }) } func businessHandler (w http.ResponseWriter, r *http.Request) { traceID, _ := r.Context().Value(traceIDKey{}).(string ) log.Printf("[trace=%s] handling request" , traceID) }
值会从父级继承 func inherited () { parent := context.WithValue(context.Background(), traceIDKey, "trace-1" ) child, cancel := context.WithTimeout(parent, time.Second) defer cancel() fmt.Println(child.Value(traceIDKey)) }
带超时、取消传播、traceID 的任务调度 package mainimport ( "context" "errors" "fmt" "sync" "time" ) type traceKey struct {}func task1 (ctx context.Context) error { select { case <-time.After(500 * time.Millisecond): fmt.Println("task1 ok, trace =" , ctx.Value(traceKey{})) return nil case <-ctx.Done(): return ctx.Err() } } func task2 (ctx context.Context) error { select { case <-time.After(200 * time.Millisecond): return errors.New("task2: simulated failure" ) case <-ctx.Done(): return ctx.Err() } } func task3 (ctx context.Context) error { select { case <-time.After(5 * time.Second): return nil case <-ctx.Done(): fmt.Println("task3 被取消, trace =" , ctx.Value(traceKey{})) return ctx.Err() } } func main () { ctx := context.WithValue(context.Background(), traceKey{}, "req-2024-0001" ) ctx, cancelTimeout := context.WithTimeout(ctx, 3 *time.Second) defer cancelTimeout() ctx, cancelAll := context.WithCancel(ctx) defer cancelAll() var wg sync.WaitGroup errCh := make (chan error, 3 ) tasks := []func (context.Context) error {task1, task2, task3} for i, t := range tasks { wg.Add(1 ) go func (id int , f func (context.Context) error ) { defer wg.Done() if err := f(ctx); err != nil { errCh <- fmt.Errorf("task%d: %w" , id, err) cancelAll() } }(i, t) } go func () { wg.Wait() close (errCh) }() for err := range errCh { fmt.Println("收到错误:" , err) } fmt.Println("所有任务结束,ctx.Err() =" , ctx.Err()) }
运行结果预期 :task1 ok, trace = req-2024-0001 收到错误: task2: simulated failure task3 被取消, trace = req-2024-0001 收到错误: task3: context canceled 所有任务结束,ctx.Err() = context canceled
这个小例子集中演示了:
WithValue 跨 goroutine 共享元数据;
WithTimeout 提供兜底的超时;
WithCancel 让”一错全错”的快速失败成为可能;
ctx.Done() 让阻塞操作及时退出,避免 goroutine 泄漏。
最佳实践与常见坑 最佳实践
context 作为函数的第一个参数 ,命名通常为 ctx context.Context。
不要把 context 存到 struct 字段里 (HTTP Request 里的除外)。
cancel 一定要调用 ,最常用 defer cancel()。go vet 的 lostcancel 会检查。
不要传 nil ,占位用 context.TODO()。
WithValue 的 key 用未导出自定义类型 ,避免包间冲突。
WithValue 只用于请求作用域元数据 ,不用于传业务参数。
父 ctx 的取消会传播到子 ,注意叶子端要及时 select 监听 ctx.Done()。
不要在 context 上保存可变数据 ;如果需要可变状态,用别的机制。
常见坑
坑
说明
规避
忘记调用 cancel
内存泄漏 / timer 泄漏
defer cancel()
把 nil 传进 WithCancel
panic
用 Background/TODO
用 string 做 WithValue key
包间冲突
自定义未导出类型
在循环里 WithValue 存大量数据
内存膨胀 & 线性查找开销
只放少量、常量 key
ctx.Done() 返回 nil 时 select
永久阻塞
用会取消的 ctx,或用 default 处理
传 &ctx 或存到 struct 里
语义混乱、难以追踪生命周期
参数透传
WithDeadline 传入过去时间
立即取消
注意父 deadline 更早也会立即取消
把 context 当作通用”依赖注入”容器
可维护性差
用显式参数或专用结构体
Go 1.20+ 补充(延伸)
context.WithCancelCause / WithTimeoutCause / WithDeadlineCause:可以附带自定义取消原因 ,ctx.Err() 返回 Canceled,但可以通过 context.Cause(ctx) 拿到原始错误,非常适合追踪”到底为什么被取消”。
Go 1.21 引入的 context.AfterFunc(ctx, f):在 ctx 取消时执行一个回调函数,用于优雅清理 (关闭连接、释放资源)。
Go 1.21 引入的 context.WithoutCancel(ctx):派生一个”不会被父取消影响”的 ctx,但保留 value,适合”即使父请求结束也要继续执行的后台任务”。
一张表总结
API
返回值
是否必须手动 cancel
主要用途
典型场景
Background()
Context
否
根 context
main、初始化
TODO()
Context
否
占位
待定上下文
WithCancel(parent)
(Context, CancelFunc)
是
手动取消
快速失败、主动中止、取消传播
WithTimeout(parent, d)
(Context, CancelFunc)
是
相对时长超时
下游调用、DB 查询、API 超时
WithDeadline(parent, t)
(Context, CancelFunc)
是
绝对截止时间
多步骤/会话共享截止时间
WithValue(parent, k, v)
Context
否(由父 cancel 管)
传递请求元数据
traceID、userID、tenantID
掌握这六个 API 及其生命周期管理,就能在 Go 并发编程中正确处理”取消、超时、跨 goroutine 数据传递”这三类核心问题。
Context 接口四大方法详解:Deadline、Done、Err、Value type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key any) any }
这四个方法是所有 context 能力的”出口”:Deadline 看时限 、Done 等信号 、Err 查原因 、Value 取数据 。理解它们之间的配合关系,才是真正会用 context。
先建立整体认知
方法
返回值
触发时机
是否阻塞
主要用途
Deadline()
(time.Time, bool)
创建时即可读
否
查询截止时刻,做提前判断/优化
Done()
<-chan struct{}
取消/超时时被 close
阻塞(配 select)
等待取消信号
Err()
error
Done 关闭后才有值
否
区分”被取消”还是”超时”
Value(key)
any
创建时即可读
否
读取请求作用域元数据
它们的关系可以概括为:
┌──────────────┐ 创建 context ──▶ │ ctx │ └──────┬───────┘ │ ┌────────────────────┼─────────────────────┐ ▼ ▼ ▼ Deadline() Done() Value() 查询截止时间 等待取消信号 读取元数据 │ ▼ (Done 被 close 之后) Err() 判断取消原因
关键理解 :
Done() 是通知机制 (channel close)。
Err() 是 Done() 的原因说明 ,必须在 Done() 关闭后才调用才有意义。
Deadline() 是提前查询 ,让你在阻塞前就能做决策。
Value() 是旁路数据通道 ,与取消机制完全独立。
下面逐一拆解。
Deadline() —— 查询截止时刻 作用与语义 Deadline() (deadline time.Time, ok bool )
返回该 context 的截止时间 。
如果 context 没有设置截止时间 ,返回 (time.Time{}, false),即零值和 false。
Background()、TODO()、WithCancel()、WithValue() 返回的 ctx 都没有 deadline ,ok=false。
WithTimeout、WithDeadline 返回的 ctx 有 deadline,ok=true。
子 context 会继承父 context 的 deadline ,且如果父的 deadline 更早,子以自己的为准(取更早的)。
典型应用场景
场景
用法
发起下游请求前判断剩余时间是否足够
if remain < minNeeded { return err }
动态设置子操作超时
根据剩余时间给每个子请求分配 budget
批处理/循环中避免做无用功
剩余时间不够就提前退出
日志/监控中记录预算
打印剩余时间做观测
注意:Deadline 只是一个”查询” ,它本身不会阻塞也不会通知。真正的取消信号来自 Done()。
查询是否有 deadline,以及剩余时间 package mainimport ( "context" "fmt" "time" ) func inspectDeadline (name string , ctx context.Context) { deadline, ok := ctx.Deadline() if !ok { fmt.Printf("[%s] 没有设置 deadline\n" , name) return } fmt.Printf("[%s] deadline = %v, 剩余 = %v\n" , name, deadline.Format(time.RFC3339), time.Until(deadline).Round(time.Millisecond), ) } func main () { inspectDeadline("Background" , context.Background()) inspectDeadline("WithCancel" , mustCancel(context.Background())) ctx, cancel := context.WithTimeout(context.Background(), 2 *time.Second) defer cancel() inspectDeadline("WithTimeout" , ctx) } func mustCancel (p context.Context) context .Context { c, cancel := context.WithCancel(p) _ = cancel return c }
输出:[Background] 没有设置 deadline [WithCancel] 没有设置 deadline [WithTimeout] deadline = 2026-10-21T..., 剩余 = 2s
根据剩余时间决定是否发起下游请求 这是 Deadline() 最有价值的用法——避免明知会超时还硬发请求 。
func callDownstream (ctx context.Context, url string ) error { const estimatedCost = 800 * time.Millisecond if deadline, ok := ctx.Deadline(); ok { remaining := time.Until(deadline) if remaining < estimatedCost { return fmt.Errorf("剩余时间 %v 不足以发起请求(需要 %v)" , remaining.Round(time.Millisecond), estimatedCost) } } return nil } func main () { ctx, cancel := context.WithTimeout(context.Background(), 500 *time.Millisecond) defer cancel() if err := callDownstream(ctx, "https://api.example.com" ); err != nil { fmt.Println("跳过请求:" , err) } }
多步骤批处理时动态分配剩余预算 func processBatch (ctx context.Context, items []string ) { for i, item := range items { deadline, ok := ctx.Deadline() if !ok { process(item) continue } remaining := time.Until(deadline) remainingItems := len (items) - i if remaining < time.Duration(remainingItems)*100 *time.Millisecond { fmt.Printf("剩余时间不足,处理完 %d/%d 项后提前退出\n" , i, len (items)) return } process(item) } }
坑与注意
不要在 ok=false 时使用返回的 time.Time ,零值的时间是 0001-01-01,容易造成错误判断。
time.Until(deadline) 可能为负(已超时),要处理这个情况。
Deadline 只描述”未来某个时刻会取消” ,但 context 也可能被提前手动取消 (父 cancel() 被调用)。所以不能只靠 Deadline 判断是否还被需要 ——真正的取消信号永远看 Done()。
Done() —— 等待取消信号(核心) 作用与语义
返回一个只读 channel 。
当 context 被取消 (手动 cancel 或超时)时,该 channel 会被 close 。
只是 close,不会发送任何值 。所以读取它永远拿到零值 struct{}{}。
未取消前,从它读取会阻塞 ;取消后,从它读取会立即返回 。
Background()、TODO() 返回的 ctx 的 Done() 是 nil ,意味着永远不取消。在 select 中监听 nil channel 会永久阻塞该 case。
为什么用 channel close 而不是发值
close 可以被”广播” :无论多少个 goroutine 在监听,全部同时被唤醒。
不需要消费 :不会因为没人在读而阻塞发送者。
接收零值语义清晰 :<-ch 拿到 struct{}{},配合 select 简单明了。
标准用法范式 select {case <-ctx.Done(): return ctx.Err() case result := <-workCh: return result, nil case <-time.After(d): return nil , errTimeout }
典型应用场景
场景
说明
可中断的阻塞操作
HTTP 请求、DB 查询、time.Sleep 替代、channel 等待
后台 worker 退出
循环中监听 Done(),收到信号就退出
快速失败
并发任务中任意一个失败就取消其它任务
超时控制
配合 WithTimeout,到点自动退出
优雅停机
服务收到停止信号时通知所有 handler 结束
把不可中断的操作变成可中断 time.Sleep 无法被中断,用它做”可中断等待”用 select + ctx.Done() 替换:
func badWait (ctx context.Context) { time.Sleep(5 * time.Second) } func goodWait (ctx context.Context, d time.Duration) error { select { case <-time.After(d): return nil case <-ctx.Done(): return ctx.Err() } } func main () { ctx, cancel := context.WithTimeout(context.Background(), 1 *time.Second) defer cancel() start := time.Now() err := goodWait(ctx, 5 *time.Second) fmt.Printf("耗时 %v, err = %v\n" , time.Since(start).Round(time.Millisecond), err) }
后台 worker 的退出 func worker (ctx context.Context, id int , jobs <-chan int ) { for { select { case <-ctx.Done(): fmt.Printf("worker %d 收到停止信号: %v\n" , id, ctx.Err()) return case job, ok := <-jobs: if !ok { fmt.Printf("worker %d: jobs 已关闭\n" , id) return } fmt.Printf("worker %d 处理 job %d\n" , id, job) } } } func main () { ctx, cancel := context.WithCancel(context.Background()) jobs := make (chan int , 10 ) for i := 1 ; i <= 3 ; i++ { go worker(ctx, i, jobs) } for i := 1 ; i <= 5 ; i++ { jobs <- i } time.Sleep(500 * time.Millisecond) cancel() time.Sleep(200 * time.Millisecond) }
多个 goroutine 同时被一个 Done 唤醒(广播语义) func main () { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup for i := 0 ; i < 5 ; i++ { wg.Add(1 ) go func (id int ) { defer wg.Done() <-ctx.Done() fmt.Printf("goroutine %d 被唤醒\n" , id) }(i) } time.Sleep(100 * time.Millisecond) cancel() wg.Wait() }
输出:goroutine 0 被唤醒 goroutine 3 被唤醒 goroutine 1 被唤醒 goroutine 4 被唤醒 goroutine 2 被唤醒
这就是 close 的广播能力 ——cancel() 只需要 close 一次,所有监听者都能感知。
Done() 为 nil 的坑func badSelect () { ctx := context.Background() select { case <-ctx.Done(): fmt.Println("永远不会执行" ) } } func fixSelect () { ctx := context.Background() select { case <-ctx.Done(): fmt.Println("取消" ) case <-time.After(100 * time.Millisecond): fmt.Println("超时兜底" ) } }
结论 :如果一个 ctx 可能返回 nil 的 Done(),要么换成 WithCancel/WithTimeout 等非空 的 context,要么在 select 里加上 default 或超时分支。
3.9 与 Deadline() 的关系
Deadline() 让你提前知道未来会取消 。
Done() 让你真正被通知取消 。
有 deadline 的 ctx 到点后,Done() 会自动被 close ,然后 Err() 返回 DeadlineExceeded。
Err() —— 查询取消原因 作用与语义
Done() 尚未关闭时 ,返回 nil。
Done() 关闭后 ,返回具体的取消原因:
context.Canceled:被手动 cancel() 取消(或因父 ctx 被取消传播)。
context.DeadlineExceeded:超时 导致取消。
(Go 1.20+ 通过 WithCancelCause 派生的 ctx,Err() 仍返回上面两个之一,但 context.Cause(ctx) 可以拿到自定义原因 。)
为什么要用 Err() 而不是直接判断错误
ctx.Done() 只能告诉你”取消了”,不能区分是主动取消还是超时 。
很多业务逻辑需要对超时 和主动取消 做不同处理:
超时 → 记录慢请求、告警、重试。
主动取消 → 客户端断开,通常不必告警、直接退出即可。
Err() 提供标准化的错误值,可以用 errors.Is 判断。
典型应用场景
场景
用法
区分”超时”和”用户取消”做不同处理
errors.Is(ctx.Err(), context.DeadlineExceeded)
日志/监控分类
记录取消原因
向上层返回错误
return ctx.Err() 让调用方判断
判断是否值得重试
超时也许能重试,主动取消通常不重试
区分取消原因做不同处理 func doWork (ctx context.Context) error { select { case <-time.After(3 * time.Second): return nil case <-ctx.Done(): return ctx.Err() } } func handleError (ctx context.Context, err error) { switch { case errors.Is(err, context.Canceled): fmt.Println("❌ 请求被主动取消(可能是客户端断开)" ) case errors.Is(err, context.DeadlineExceeded): fmt.Println("⏰ 请求超时(可能是下游太慢,考虑告警/重试)" ) case err == nil : fmt.Println("✅ 成功" ) default : fmt.Printf("⚠️ 其他错误: %v\n" , err) } } func main () { ctx1, cancel1 := context.WithTimeout(context.Background(), 1 *time.Second) defer cancel1() handleError(ctx1, doWork(ctx1)) ctx2, cancel2 := context.WithCancel(context.Background()) go func () { time.Sleep(200 * time.Millisecond) cancel2() }() handleError(ctx2, doWork(ctx2)) }
输出:⏰ 请求超时(可能是下游太慢,考虑告警/重试) ❌ 请求被主动取消(可能是客户端断开)
错误的正确包装与向上传递 func getUser (ctx context.Context, id int ) (*User, error) { user, err := dbQuery(ctx, id) if err != nil { if errors.Is(err, context.DeadlineExceeded) { return nil , fmt.Errorf("getUser(%d): %w" , id, context.DeadlineExceeded) } return nil , fmt.Errorf("getUser(%d): %w" , id, err) } return user, nil } func handler (ctx context.Context) { _, err := getUser(ctx, 42 ) if errors.Is(err, context.DeadlineExceeded) { } }
Err() 与 Done() 的时序func checkTiming () { ctx, cancel := context.WithCancel(context.Background()) fmt.Println("取消前:" , ctx.Err()) go func () { time.Sleep(100 * time.Millisecond) cancel() }() <-ctx.Done() fmt.Println("取消后:" , ctx.Err()) }
规则 :Err() 只有在 Done() 关闭后才可能非 nil。所以典型的写法是先 <-ctx.Done() 再读 ctx.Err() ,或者直接 select 的 case <-ctx.Done(): return ctx.Err()。
Go 1.20+ 自定义取消原因(延伸) func advancedCancel () { ctx, cancel := context.WithCancelCause(context.Background()) cancel(errors.New("用户余额不足" )) <-ctx.Done() fmt.Println("ctx.Err() =" , ctx.Err()) fmt.Println("context.Cause(ctx) =" , context.Cause(ctx)) }
注意 :Err() 永远只返回 Canceled / DeadlineExceeded,自定义原因要读 context.Cause() 。这是 Go 1.20 之后排查问题的新利器。
Value() —— 读取请求作用域元数据 作用与语义
沿着 context 链向上查找 key 对应的 value。
找到最近一层的 value 就返回;找不到返回 nil。
线性查找 (通过链上的 map 逐层找),性能 O(层数)。
key 必须可比较(comparable) ,且不能为 nil ,否则 panic 。
返回类型是 any,需要做类型断言 。
核心原则(务必牢记)
key 用未导出的自定义类型 ,避免跨包冲突。
❌ context.WithValue(ctx, "userID", 42)
✅ context.WithValue(ctx, userIDKey{}, 42)
只放请求作用域元数据 ,如 traceID、userID、tenantID、语言偏好。
不要放业务参数 (应该作为函数参数显式传递)。
不要放可变数据 (并发安全问题)。
不要放大量数据 (线性查找 + 内存膨胀)。
典型应用场景
场景
说明
分布式追踪
traceID / spanID,跨 goroutine 传递
认证信息
userID、角色、token(注意安全)
多租户
tenantID
请求级别配置
语言、时区、feature flag
中间件向下游透传
HTTP middleware → handler → service → repo
基础用法 + 类型安全封装 package mainimport ( "context" "fmt" ) type ctxKey int const ( traceIDKey ctxKey = iota userIDKey ) func WithTraceID (ctx context.Context, id string ) context .Context { return context.WithValue(ctx, traceIDKey, id) } func TraceIDFrom (ctx context.Context) (string , bool ) { v, ok := ctx.Value(traceIDKey).(string ) return v, ok } func WithUserID (ctx context.Context, id int64 ) context .Context { return context.WithValue(ctx, userIDKey, id) } func UserIDFrom (ctx context.Context) (int64 , bool ) { v, ok := ctx.Value(userIDKey).(int64 ) return v, ok } func main () { ctx := context.Background() ctx = WithTraceID(ctx, "trace-abc-123" ) ctx = WithUserID(ctx, 42 ) if traceID, ok := TraceIDFrom(ctx); ok { fmt.Println("traceID =" , traceID) } if userID, ok := UserIDFrom(ctx); ok { fmt.Println("userID =" , userID) } fmt.Println("未设置的 key 断言:" , ctx.Value("missing" )) }
链式继承 —— 子 ctx 能读到父的 value func chainValue () { root := context.WithValue(context.Background(), traceIDKey, "trace-001" ) withTimeout, cancel := context.WithTimeout(root, time.Second) defer cancel() fmt.Println("child 读到:" , withTimeout.Value(traceIDKey)) child := context.WithValue(withTimeout, traceIDKey, "trace-002" ) fmt.Println("child 覆盖后:" , child.Value(traceIDKey)) fmt.Println("父仍然原值:" , withTimeout.Value(traceIDKey)) }
要点 :
WithValue 不会修改原 ctx ,而是返回一个新的 ctx,带一层额外映射。
查找时从近到远 ,最近的有效。
模拟 HTTP 中间件透传 traceID type traceIDKeyType struct {} func TraceMiddleware (next http.Handler) http .Handler { return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { traceID := r.Header.Get("X-Trace-ID" ) if traceID == "" { traceID = generateTraceID() } ctx := context.WithValue(r.Context(), traceIDKeyType{}, traceID) next.ServeHTTP(w, r.WithContext(ctx)) }) } func BusinessHandler (w http.ResponseWriter, r *http.Request) { traceID, _ := r.Context().Value(traceIDKeyType{}).(string ) log.Printf("[trace=%s] handling request" , traceID) if err := queryDB(r.Context()); err != nil { } }
不要滥用 Value —— 反模式对比 func BadCreateUser (ctx context.Context) error { name := ctx.Value("name" ).(string ) age := ctx.Value("age" ).(int ) return saveUser(name, age) } func GoodCreateUser (ctx context.Context, name string , age int ) error { traceID, _ := ctx.Value(traceIDKey{}).(string ) log.Printf("[trace=%s] create user name=%s age=%d" , traceID, name, age) return saveUser(name, age) }
Value 的性能与浅坑func benchmarkValue () { ctx := context.Background() for i := 0 ; i < 1000 ; i++ { ctx = context.WithValue(ctx, ctxKey(i), i) } _ = ctx.Value(ctxKey(999 )) }
建议 :
元数据控制在个位数 。
频繁读取的 value 可在 handler 入口一次取出 放入局部变量,避免反复 Value() 查找。
类型断言的防御式写法 if v, ok := ctx.Value(traceIDKey).(string ); ok && v != "" { }
Value() 返回 nil 时若直接 .(string) 会 panic,务必用 , ok 形式。
四方法协同:完整示例 下面用一个完整的服务调用链 把四个方法都用上:
package mainimport ( "context" "errors" "fmt" "time" ) type traceKey struct {}func callDownstream (ctx context.Context) (string , error) { traceID, _ := ctx.Value(traceKey{}).(string ) if deadline, ok := ctx.Deadline(); ok { if time.Until(deadline) < 300 *time.Millisecond { return "" , fmt.Errorf("[trace=%s] 剩余时间不足,跳过下游调用" , traceID) } } select { case <-time.After(1 * time.Second): return fmt.Sprintf("[trace=%s] downstream ok" , traceID), nil case <-ctx.Done(): if errors.Is(ctx.Err(), context.DeadlineExceeded) { return "" , fmt.Errorf("[trace=%s] 下游调用超时: %w" , traceID, ctx.Err()) } return "" , fmt.Errorf("[trace=%s] 下游调用被取消: %w" , traceID, ctx.Err()) } } func handleRequest (ctx context.Context) (string , error) { traceID, _ := ctx.Value(traceKey{}).(string ) fmt.Printf("[trace=%s] 开始处理\n" , traceID) return callDownstream(ctx) } func main () { func () { ctx := context.WithValue(context.Background(), traceKey{}, "REQ-A" ) ctx, cancel := context.WithTimeout(ctx, 2 *time.Second) defer cancel() result, err := handleRequest(ctx) fmt.Printf("A 结果: %q, err=%v\n\n" , result, err) }() func () { ctx := context.WithValue(context.Background(), traceKey{}, "REQ-B" ) ctx, cancel := context.WithTimeout(ctx, 500 *time.Millisecond) defer cancel() result, err := handleRequest(ctx) fmt.Printf("B 结果: %q, err=%v\n\n" , result, err) }() func () { ctx := context.WithValue(context.Background(), traceKey{}, "REQ-C" ) ctx, cancel := context.WithDeadline(ctx, time.Now().Add(100 *time.Millisecond)) defer cancel() result, err := handleRequest(ctx) fmt.Printf("C 结果: %q, err=%v\n\n" , result, err) }() func () { ctx := context.WithValue(context.Background(), traceKey{}, "REQ-D" ) ctx, cancel := context.WithCancel(ctx) go func () { time.Sleep(200 * time.Millisecond) cancel() }() result, err := handleRequest(ctx) fmt.Printf("D 结果: %q, err=%v\n" , result, err) }() }
输出(示意):[trace=REQ-A] 开始处理 A 结果: "[trace=REQ-A] downstream ok", err=<nil> [trace=REQ-B] 开始处理 B 结果: "", err=[trace=REQ-B] 下游调用超时: context deadline exceeded [trace=REQ-C] 开始处理 C 结果: "", err=[trace=REQ-C] 剩余时间不足,跳过下游调用 [trace=REQ-D] 开始处理 D 结果: "", err=[trace=REQ-D] 下游调用被取消: context canceled
这段代码完整演示了:
Value 传递 traceID,贯穿整个调用链;
Deadline 提前判断预算,避免无效请求;
Done 让阻塞的等待可以被立即中断;
Err 区分了”超时”与”主动取消”,支持不同错误处理策略。
常见坑速查 Deadline 相关
坑
规避
ok=false 时使用了零值时间
先判断 ok
以为 Deadline() 会阻塞
它只是查询,阻塞请用 Done()
只靠 Deadline 判断是否被取消
手动 cancel() 也会取消,要监听 Done()
Done 相关
坑
规避
监听 nil channel 永久阻塞
用非空 ctx 或加 default/超时兜底
用 <-ctx.Done() 而不用 select 做多路等待
用 select 同时监听其他 channel
在 Done() 上读取值
close 不发值,读到的是零值
Err 相关
坑
规避
Done 未关闭时就读 Err()
先 <-ctx.Done()
用字符串比较错误
用 errors.Is(err, context.Canceled)
忘记包装导致错误链断掉
fmt.Errorf("...: %w", ctx.Err())
需要自定义原因却只看 Err()
Go 1.20+ 用 context.Cause(ctx)
Value 相关
坑
规避
用 string/int 做 key
自定义未导出类型(type ctxKey int 或 struct{})
key 为 nil
永远不要传 nil
直接类型断言
用 v, ok := ctx.Value(k).(T)
用 Value 传业务参数
业务参数用函数入参
放大量数据
只放少量元数据
每层反复取 value
handler 入口取一次存局部变量
总结对照表
方法
何时读
是否需要阻塞
关键点
一句话用途
Deadline()
发起操作前
否
返回 (时间, ok),父 deadline 会传播
提前决策 :剩余时间够不够
Done()
等待时
是 (配 select)
只 close 不发值;nil 会永久阻塞
等待取消 :可中断阻塞
Err()
Done() 关闭后
否
返回 Canceled/DeadlineExceeded
查询原因 :区分取消类型
Value(key)
任意时刻
否
链式查找;key 用未导出类型
读取元数据 :traceID、userID
记住一条主线 :
创建时用 WithXxx 塑造取消行为 → 使用时用 Deadline() 做提前判断 ,用 Done() 阻塞等待取消 ,用 Err() 区分原因 ,用 Value() 旁路传递元数据 。
至此,Context 接口的创建侧(Background/TODO/WithCancel/WithTimeout/WithDeadline/WithValue)和消费侧(Deadline/Done/Err/Value)就完整闭环了。日常开发中绝大多数并发控制问题,都能用这一套模型清晰表达。