超时传递需显式透传、解析、派生,HTTP中X-Timeout-Ms须手动计算写入并由下游转为context;gRPC中grpc-timeout需拦截器协同处理;cancel()必须显式调用,禁止依赖defer或time.After()。

超时传递不是“自动发生”的事,它必须在每一跳显式透传、解析、派生,漏掉任意一环,下游就收不到 cancel 信号。
HTTP 调用中 X-Timeout-Ms 必须手动计算并写入 header
HTTP 协议本身不带 deadline 语义,context.WithTimeout 的 deadline 不会自动变成请求头。你得自己算、自己塞、下游自己读、自己转回 context——四步缺一不可。
- 上游:调用
ctx.Deadline()算出剩余毫秒数(注意用time.Until(),别用time.Now().Add(...)反推,避免时区/精度误差),写入X-Timeout-Msheader - 下游:收到后检查该 header 是否存在且为数字,再用
time.Now().Add(time.Duration(ms) * time.Millisecond)构造新ctx;别直接context.WithTimeout(ctx, time.Duration(ms)*time.Millisecond),否则会叠加超时 - 若 header 缺失或解析失败,应 fallback 到默认短超时(如 500ms),而不是沿用父 ctx —— 否则可能继承一个已过期的 deadline
- 务必在 HTTP client 层统一拦截,不要让每个业务 handler 自己拼 header,容易漏或格式不一致
gRPC 中 grpc-timeout metadata 必须由拦截器处理
gRPC 协议原生支持超时透传,但只认标准 key grpc-timeout 和固定格式 value(如 "200m"),且依赖客户端和服务端拦截器协同工作。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 客户端拦截器里必须调用
metadata.AppendToOutgoingContext(ctx, "grpc-timeout", "200m"),不能用自定义 key(如timeout-ms) - 服务端拦截器必须调用
grpc.SendHeader()或grpc.SetHeader()才会触发 gRPC-Go 内部对grpc-timeout的解析;否则ctx.Err()始终为nil - gRPC-Go v1.60+ 对 value 格式更严格:单位只能是
h/m/s/ms/us/ns,含空格或非法单位(如"200 ms")会被静默丢弃 - 别在 handler 里重复调
context.WithTimeout—— 入口拦截器已基于grpc-timeout派生了带 deadline 的 ctx,直接用即可
并发 goroutine 中 cancel() 必须显式调用,不能依赖 defer
很多人以为 defer cancel() 就万事大吉,但在高并发场景下,goroutine 提前返回、panic、或 error early return 时,defer 可能根本没执行,导致 timer 泄漏。
- 所有
context.WithTimeout都要配对调cancel(),且最好放在最外层逻辑出口处(比如函数末尾、error 分支、成功分支) - 如果 goroutine 是通过 channel 接收任务,且可能被多次复用,每次任务结束都应调一次
cancel(),否则上一轮的 timer 会继续跑完 - 不要用
time.After()替代 context 超时:它无法取消,每次调用都启一个 goroutine,高并发下易堆积 - 子 goroutine 必须继承父 ctx,禁止用
context.Background()重新开始计时 —— 这等于切断链路
真正难的不是设 timeout,而是确保每条链路上的 deadline 都被正确截断、转换、再注入。一个没解析 X-Timeout-Ms 的中间服务,或一个忘了调 SendHeader() 的 gRPC 拦截器,就足以让整条链路失去超时控制。这东西没法靠测试全覆盖,得靠代码审查和统一 SDK 强制约束。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










