context 传不下去主因是未显式传递,需用 r.withcontext() 更新请求;key 应为私有类型;读取须封装安全函数;withtimeout 后必须 defer cancel() 防泄漏;自定义循环需 select 监听 done()。

context 传不下去,90% 是因为没把新 context 显式传给下游——它不会自动覆盖、不会隐式更新、更不会跨 goroutine 自动透传。
中间件里塞了值,Handler 却读不到
根本原因是 r.Context() 是只读副本,中间件里直接 ctx = context.WithValue(r.Context(), key, val) 不会改变请求对象本身。下游拿到的还是原始 r.Context(),自然读不到。
- 必须用
r = r.WithContext(ctx)生成新请求并向下传递(比如传给next.ServeHTTP()) - key 类型不能是
string,得定义私有类型:如type userIDKey struct{},否则不同包用相同字符串 key 会互相覆盖 - 写入后不检查返回值是否为
nil就强断言,比如ctx.Value(userIDKey{}).(int64),一旦 key 类型或值类型不匹配,运行时 panic - 推荐封装读取函数:
func GetUserID(ctx context.Context) (int64, bool) { v, ok := ctx.Value(userIDKey{}).(int64); return v, ok }
WithTimeout 后忘记 defer cancel() 导致 goroutine 泄漏
context.WithTimeout 超时后只是关闭 Done() 通道、设置 Err(),但内部 time.Timer 仍在运行,关联 goroutine 不退出,引用无法被 GC。
- 永远用两个变量接收:
ctx, cancel := context.WithTimeout(parent, 5*time.Second) - 作用域结束前必须
defer cancel(),哪怕已超时——这是释放 timer 的唯一方式 - 如果提前完成任务,也要调用
cancel(),不能等超时自动触发 - 测试时容易漏掉这步,跑完压测发现 goroutine 数持续上涨,就是这个原因
自己写的 for-select 循环不响应取消
标准库函数(如 http.Client.Do、db.QueryRowContext)内部会监听 ctx.Done(),但你自己写的阻塞逻辑不会自动响应。
- 不能写
for ctx.Err() == nil { ... }——ctx.Err()在取消前恒为nil,等价于死循环 - 必须用
select显式监听:case - 循环体内如果有耗时操作(如
time.Sleep、chan recv),每次迭代都应插入一次select判断 - 别依赖“函数返回后再检查 err”,有些操作可能卡在 channel 等待上,永远不返回
为什么 valueCtx 里的值突然变成 nil
context.WithValue 返回的是一个全新 context,原 context 完全不变。常见错误是调用后没把返回值继续往下传。
- 函数 A 调用
ctx = context.WithValue(ctx, k, v),但没把ctx当参数传给函数 B → B 拿到的还是旧 context - goroutine 启动时没显式传入 context:
go worker(ctx)忘了写ctx,子 goroutine 用的是闭包捕获的老 context 或context.Background() - HTTP handler 中从
r.Context()取值,但中间件没调用r.WithContext()更新,就等于没传 - value 类型不可比较(如
[]byte、map[string]int)会导致ctx.Value()panic,只能存int、string、结构体等可比较类型
真正难的不是记住 API,而是理解 context 是不可变的、是单向传递的、是需要你每一层都亲手“递”下去的——少一次赋值、少一次传参、少一次 defer cancel(),链就断了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











