必须从 http.Request.Context() 派生 context,不可用 context.Background() 或全局变量;handler 入口立即获取,显式传给 DB/HTTP 等下游调用,并确保各组件响应 ctx.Err()。

Go HTTP handler 中如何正确传递 context.Context?
不能直接用 context.Background() 或全局 context,必须从 http.Request.Context() 派生。否则请求取消、超时信号无法透传到底层调用链。
常见错误是把 request 的 context 存成包级变量或缓存起来复用,这会导致不同请求的 cancel 互相干扰,甚至泄露 goroutine。
- 每个 handler 入口立即用
r.Context()获取当前请求 context - 所有下游调用(DB 查询、HTTP 调用、channel 操作)都显式传入该 context
- 避免在 goroutine 中直接捕获外部 context 变量,应作为参数传入:错误写法
go func() { db.Query(ctx, ...) }()→ 正确写法go func(ctx context.Context) { db.Query(ctx, ...) }(r.Context())
为什么 http.TimeoutHandler 不能替代 context.WithTimeout?
http.TimeoutHandler 只终止 response writer 的写入,并不会通知 handler 内部逻辑停止执行。goroutine 仍可能持续运行、占用资源、甚至完成无意义的 DB 查询。
真正可控的超时必须靠 context.WithTimeout 或 context.WithDeadline,配合各组件对 context 的主动响应(如 db.QueryContext、http.Client.Do)。
立即学习“go语言免费学习笔记(深入)”;
-
http.TimeoutHandler是外层“熔断”,适合防止阻塞整个 server;context.WithTimeout是内层“协作取消”,用于精准释放资源 - 两者可以共存:外层设宽松 timeout(如 30s),内层设业务级 timeout(如 5s DB 查询 + 3s 依赖服务)
- 注意:
context.WithTimeout的 timer 是 per-request 的,不要在 init 或包变量中提前创建
database/sql 中 QueryContext 与普通 Query 的关键区别
不带 Context 的 db.Query 完全忽略请求取消信号,即使前端已断开连接,查询仍会跑完并返回结果(或 panic)。而 db.QueryContext 在 context 被 cancel 或超时时,会主动中断底层连接读取,并返回 context.Canceled 或 context.DeadlineExceeded 错误。
- MySQL 驱动需 v1.6.0+,PostgreSQL 驱动需 v1.12.0+ 才完整支持 cancel 语义
- 调用
rows.Close()仍是必须的,否则连接不会归还连接池;但QueryContext失败后,rows可能为 nil,要判空 - 示例:
rows, err := db.QueryContext(r.Context(), "SELECT * FROM users WHERE id = ?", id)—— 错误不是超时本身,而是你没检查 err 是否为 context 相关错误
中间件中 context.Value 的使用边界在哪?
context.WithValue 只适合传极少量、只读、无副作用的请求元数据(如用户 ID、trace ID、request ID),绝不能用来传 service 实例、DB 连接、logger 对象等可变或重量级对象。
滥用 context.Value 会导致代码难以测试、调试困难、内存泄漏(值未被 GC,因 context 生命周期长于预期)。
- 优先用函数参数传参;只有跨多层中间件且无法改签名时,才考虑
context.WithValue - key 必须是自定义类型(如
type userIDKey struct{}),避免字符串 key 冲突 - 永远不要在 context 中存指针或接口类型变量,除非你 100% 确保它不会被并发修改
链路取消和资源回收真正的难点不在 API 调用,而在每一层是否真的响应了 Done channel —— 很多第三方库默认不监听 context,需要你手动包装或换库。


















