context 必须显式传参且不可缓存,漏传会导致下游收不到取消信号;withvalue 返回新 context,旧 context 不变;key 应用未导出空结构体而非 string;withtimeout/withcancel 的 cancel 必须 defer 调用以防 goroutine 泄漏;goroutine 启动时必须传入 ctx;监听 done() 后需检查 ctx.err() 并区分错误类型;所有 i/o 操作都必须接入当前 ctx。

Context 必须显式传参,不能塞进 struct、不能缓存、不能漏传——漏一次,下游就收不到取消信号。
为什么 ctx.Value 拿不到值
根本不是 API 写错了,而是误以为 context.WithValue 会修改原 context。它返回的是一个新 context,旧变量不变。
- 错误写法:
context.WithValue(ctx, key, val)后没把新ctx传给下游函数,下游仍用旧ctx调用ctx.Value(key)→ 返回nil - HTTP handler 中更隐蔽:
r.Context()是只读副本,必须显式调用r = r.WithContext(newCtx)才能让后续中间件拿到新值 - 取值时没做双返回值断言,比如直接
ctx.Value(key).(string),一旦 key 不存在或类型不匹配,运行时 panic
key 类型为什么不能用 string
用 string 当 key 看似简单,实则埋雷:编译期完全不检查,拼错就静默失效。
- 正确做法是定义未导出空结构体:
type userIDKey struct{},哪怕只用一次 - 存取时用
context.WithValue(ctx, userIDKey{}, u)和ctx.Value(userIDKey{}).(User) - 好处:编译器能校验类型;不同包定义同名 struct 不冲突(类型不同);IDE 可跳转到定义,比搜字符串靠谱得多
- 绝对不要传
[]byte、map、指针等不可比较类型——context.Value要求 key 可比较,否则运行时 panic
WithTimeout / WithCancel 的 cancel 必须 defer 调用
漏掉 defer cancel() 不只是“忘了清理”,而是直接导致 goroutine 泄漏——timerCtx 内部的 *time.Timer 不会被 GC 回收。
-
context.WithTimeout创建的cancel函数在超时后仍可安全调用(幂等),但不调用就泄漏 -
context.WithCancel的cancel一旦执行,再次调用会 panic(除非你加了防护) - 常见误用:在
for-select循环里反复调用context.WithTimeout(ctx, time.Second),每次新建 timer,泄漏累积 - 多个 goroutine 共享同一个子 context 时,任一调用
cancel()都会使全部监听者退出,这点要提前设计好协作逻辑
goroutine 启动时必须显式传入 ctx
Context 不自动跨 goroutine 继承。你在主线程派生了带 timeout 的 context,但启动子 goroutine 时没传,那它就永远收不到 ctx.Done() 信号。
- 错误模式:
go doWork()—— 没传ctx,子 goroutine 只能用context.Background(),彻底脱离请求生命周期 - 正确姿势:
go doWork(ctx),且如果需要额外控制(如独立超时),应在启动前派生:childCtx, _ := context.WithTimeout(ctx, 500*time.Millisecond),再传childCtx - 监听
ctx.Done()时,仅select不够,必须立刻检查err := ctx.Err(),再用errors.Is(err, context.Canceled)或errors.Is(err, context.DeadlineExceeded)区分退出原因,否则无法做针对性清理
最易被忽略的一点:所有 I/O 操作(http.Client.Do、sql.DB.QueryContext、time.Sleep)都必须传当前 ctx,否则超时和取消就形同虚设。不是“用了 context”就算数,是每个阻塞点都要接入。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











