必须使用 r.context() 而非 context.background(),因为 r.context() 自动绑定请求生命周期,能在客户端断连或超时时立即触发 done() 信号,实现链路级取消;若用 context.background() 则切断取消传播,导致 goroutine 和资源泄漏。

Go 框架中用 context 传递超时取消信号,不是“加个参数就完事”,而是必须贯穿调用链、及时响应、主动释放——漏掉任一环,超时就形同虚设,goroutine 和资源照样泄漏。
为什么 HTTP handler 里直接用 r.Context() 而不是 context.Background()
HTTP server 启动每个请求时,已自动为 *http.Request 绑定了一个带生命周期的 context。这个 context 在客户端断连、超时或服务端主动关闭连接时,会自动触发 Done()。
- 用
r.Context()可直接继承请求的天然取消信号,比如用户关掉浏览器标签页,后端 goroutine 就能立刻感知并退出 - 若改用
context.Background(),等于切断了与请求生命周期的绑定,哪怕客户端早已放弃,你的数据库查询或下游 RPC 还在傻等 - 框架如 Gin、Echo、Chi 都要求中间件和 handler 显式接收
ctx参数,本质就是强制你延续这个上下文链
context.WithTimeout 的 timeout 值该设多长?别硬编码
超时值不是拍脑袋定的,它得匹配整个链路中最弱的一环:网络 RTT、下游服务 SLA、重试策略、甚至前端可接受等待时间。
- 对内部微服务调用,建议设为下游 P99 延迟 + 100ms 缓冲,例如下游 P99 是 400ms,这里设
500 * time.Millisecond - 对外部第三方 API,务必查对方文档的 SLA;没文档?先跑压测看 p95,再乘以 1.5 做初始值
- 绝对避免全局常量如
const globalTimeout = 3 * time.Second—— 不同接口敏感度不同,登录接口可以等 2 秒,支付回调必须 800ms 内返回
调用下游时忘记传 ctx,WithTimeout 就彻底失效
常见错误是:handler 里创建了带超时的 ctx,但调用数据库或 HTTP client 时,没把 ctx 透传进去。
- HTTP 请求必须用
http.NewRequestWithContext(ctx, ...)或req.WithContext(ctx),只用http.Get()或client.Do(req)(没设 ctx)完全无视超时 - 数据库操作如
db.QueryContext(ctx, ...)、tx.ExecContext(ctx, ...)—— 标准库database/sql所有带Context后缀的方法才是可取消的 - 自定义函数必须把
ctx context.Context作为第一个参数,且内部所有阻塞操作都要监听,不能只靠外层超时“等着它自己结束”
cancel() 必须 defer 调用,但别 defer 在错误路径之后
cancel() 函数不只释放内存,更关键的是防止子 context 的 Done() 通道一直悬空,导致 GC 无法回收关联的 timer 和 goroutine。
- 正确写法:
ctx, cancel := context.WithTimeout(...); defer cancel()—— 放在函数开头,不管后续是否 panic 或 return 都会执行 - 错误写法:在
if err != nil { cancel(); return }之后才defer cancel(),这时正常流程下cancel()会被调用两次(defer + 显式),虽不 panic,但属于冗余操作,且掩盖了逻辑混乱 - 特别注意:如果函数启动了新 goroutine 并传入
ctx,cancel()仍需 defer,因为子 goroutine 应该在父函数退出时被通知终止,而不是靠自己判断
最易被忽略的点是:context 的取消信号不会自动“穿透”到 syscall 层或 Cgo 调用中。如果你封装了阻塞式系统调用(比如某些老版本 cgo 库),仅靠 ctx.Done() 监听是不够的,得配合 runtime.LockOSThread() 或改用非阻塞接口——这点在高并发 IO 密集型服务里,往往成为 goroutine 泄漏的隐形源头。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











