context不能传业务数据,只应传递请求级元数据和生命周期信号;必须用未导出空结构体作key,避免string冲突;http调用需手动透传超时header并配transport。

Context 不能用来传业务数据
直接用 context.WithValue 往 context.Context 里塞请求 ID、用户信息、配置参数这类业务数据,是 Go 官方明确不推荐的做法。Context 的设计目标是传递取消信号、超时控制和截止时间,不是通用的数据容器。
常见错误现象:代码里满屏 ctx = context.WithValue(ctx, keyUser, user),后续层层传递,最后在 handler 或 DB 层用 ctx.Value(keyUser) 取值 —— 表面能跑,但类型安全差、key 冲突风险高、调试困难、IDE 无法跳转。
真正该做的:把业务数据作为函数参数显式传入。比如 handler 接收 *http.Request,从中解析用户信息后,直接作为参数传给 service 方法;service 方法签名应是 func (s *Service) DoSomething(ctx context.Context, userID string, payload Data),而不是依赖从 ctx 里“猜”出 userID。
哪些场景下可以用 context.WithValue
极少数被 Go 团队认可的例外:传递只读的、与请求生命周期强绑定的、且无法通过参数传递的元数据,比如 tracing 的 span context、gRPC 的 metadata(经封装)、HTTP 中间件注入的认证主体(但建议用 struct 封装而非裸字段)。
使用前提:
- key 必须是私有变量(
var userKey = &struct{}{}),避免与其他包冲突 - value 必须是可比较的(不能是 map/slice/func),且最好为不可变结构体
- 必须配套提供类型安全的 getter 函数,例如
func UserFromCtx(ctx context.Context) (*User, bool)
示例:
var userKey = &struct{}{}
<p>func ContextWithUser(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}</p><p>func UserFromCtx(ctx context.Context) (<em>User, bool) {
u, ok := ctx.Value(userKey).(</em>User)
return u, ok
}
</p>
为什么 cancel/timeout 一定要用 Context 传
因为这是 Context 唯一不可替代的职责。goroutine 启动后,没有其他标准方式能安全、统一地通知它“停掉”。靠 channel 发信号要额外管理 channel 生命周期;靠全局 flag 无法做到 per-request 粒度。
典型误用:
- 在 goroutine 内部硬编码
time.Sleep(5 * time.Second)替代ctx.Done()—— 无法响应上游提前取消 - 把
context.Background()直接传进长耗时函数,却不检查ctx.Err()—— 导致 goroutine 泄漏 - 用
context.WithTimeout(ctx, d)但忘了 defer cancel() —— 导致 timer 不释放,内存缓慢增长
正确姿势:所有 I/O 操作(HTTP client、DB query、channel receive)都应接受 ctx 并参与取消链;每次调用 WithCancel/WithTimeout 后,必须确保对应 cancel() 被调用(通常用 defer)。
HTTP handler 中 Context 的生命周期陷阱
Go 的 http.Request.Context() 是 request-scoped 的,但很多人误以为它和 handler 函数生命周期一致。实际它会在请求结束(连接关闭、超时、response 写完)后自动 cancel,但如果你启动了异步 goroutine(比如发消息到 Kafka、写日志到远端),这个 goroutine 拿到的 r.Context() 一旦被 cancel,就再也不能恢复。
容易踩的坑:
- 在 handler 里起 goroutine 并直接传入
r.Context(),结果异步任务被意外中断 - 用
context.WithValue(r.Context(), k, v)后,没意识到新 ctx 仍受原 request 生命周期约束 - 想延长 context 生命周期,却错误地用
context.Background()替代 —— 彻底丢失取消能力
稳妥做法:对必须脱离 request 生命周期的任务,用 context.WithTimeout(context.Background(), 30*time.Second) 新建独立上下文,并自行管理 cancel;或改用带重试/死信机制的异步队列,而非依赖 ctx 控制。
真正难的不是怎么传,是怎么判断该不该传、传什么、谁负责 cancel —— 这些边界一旦模糊,Context 就会从工具变成债务。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











