Context传递必须显式传参,不能依赖全局变量或闭包捕获;所有I/O函数首参应为ctx context.Context,启动goroutine时须显式传入ctx,中间件需用req.WithContext()写回增强后的Context,且避免滥用WithValue传非元数据。

Context 传递必须显式传参,不能依赖全局变量或闭包捕获
Go 的 context.Context 不会自动跨 goroutine 或函数调用链传播——你写 http.HandleFunc 时传进来的 ctx,不会自动出现在你调用的 db.Query(ctx, ...) 之外的任何地方。常见错误是:在 handler 里启动 goroutine 却忘了把 ctx 传进去,导致超时/取消信号丢失。
实操建议:
- 所有可能涉及 I/O、重试、超时的函数签名,第一个参数应为
ctx context.Context - 启动新 goroutine 时,必须显式传入
ctx(哪怕只是ctx本身,或用ctx.WithTimeout衍生) - 避免在中间件或工具函数里“偷偷”从
http.Request取ctx再塞进某个全局 map——这破坏可测试性,也容易引发竞态
用 context.WithValue 传请求级数据,但别存结构体或指针
context.WithValue 是唯一合法的「带业务数据的 Context 传递」方式,但它的设计初衷是传元数据(如用户 ID、trace ID),不是替代函数参数。滥用会导致类型不安全、难以调试。
实操建议:
- 键必须是自定义 unexported 类型(比如
type userIDKey struct{}),避免字符串键冲突 - 值只传基本类型(
string,int64)、小结构体(如time.Time)或不可变指针(如*sql.DB);禁止传含 mutex、channel 或大 slice 的结构体 - 不要用
context.WithValue传 service 实例或 config——这些应该通过依赖注入(构造函数参数)传入,而不是塞进 Context
HTTP 中间件如何正确透传并增强 Context
中间件本质是包装 http.Handler,它拿到的 req.Context() 是初始上下文;你需要用 req = req.WithContext(...) 把增强后的 Context 写回去,否则下游 handler 拿不到。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
- 解析 JWT 后,用
ctx = context.WithValue(req.Context(), userKey, userID),再req = req.WithContext(ctx)赋值回 request - 添加 trace ID 时,优先用
req = req.WithContext(context.WithValue(...)),而非直接改req.Context()返回值(后者不生效) - 中间件中调用下游 handler 前,务必检查
ctx.Err() != nil,提前 return 避免无谓执行
Cancel 和 Done channel 泄漏的典型场景与修复
忘记调用 cancel() 是最隐蔽的内存泄漏源之一:每个 context.WithCancel 会启动一个 goroutine 监听 cancel 信号,若未调用 cancel(),该 goroutine 永不退出。
实操建议:
- 仅在需要主动取消的场景才用
context.WithCancel(比如长轮询、流式响应);普通 HTTP handler 用req.Context()自带的 cancel 即可 - 使用
defer cancel()必须确保cancel函数本身不会 panic(比如它来自context.WithTimeout且 timeout 已过期,调用仍安全) - 测试时用
runtime.NumGoroutine()对比前后数量,能快速暴露未关闭的 cancel goroutine
Context 传递真正难的不是语法,而是判断「这个值该不该放 Context 里」「这个 cancel 该在哪一层调」——多数问题都出在边界模糊:比如把数据库连接池放 Context,或在 middleware 里 cancel 了整个请求生命周期的 Context。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










