不能直接用 uuid.new() 做 request id,因其与请求生命周期脱钩,导致中间件、日志、下游调用无法自动获取;需通过中间件统一注入 context.context,并配合自定义 key 与类型断言安全透传。

为什么不能直接用 uuid.New() 做 Request ID?
直接在 HTTP handler 里每次调用 uuid.New() 确实能生成唯一 ID,但问题在于:它和请求生命周期脱钩。中间件、日志、下游调用都拿不到这个 ID,除非你手动层层传参——这违背 Go 的简洁哲学,也容易漏传或传错。
真正需要的是一个「请求上下文绑定的、可透传的、无感注入」的 ID。Go 的 context.Context 就是为此设计的,但必须配合中间件统一注入,否则每个 handler 都要重复写 ctx = context.WithValue(ctx, key, id),既啰嗦又易错。
用中间件 + context.WithValue 统一注入
最轻量、最可控的方式:在入口 middleware 生成 ID,并塞进 context.Context,后续所有 handler、service、db 调用只要拿到 req.Context() 就能取到它。
- 生成 ID 推荐用
uuid.NewString()(Go 1.20+),比uuid.New().String()少一次内存分配 - 定义全局
context.Key类型,避免字符串 key 冲突:type requestIDKey struct{} - 中间件里别直接用
http.Request.WithContext(),要用req = req.WithContext(context.WithValue(req.Context(), requestIDKey{}, id)) - 下游取值时务必类型断言,且检查是否为 nil:
if id, ok := req.Context().Value(requestIDKey{}).(string); ok { ... }
如何让日志自动带上 Request ID?
标准库 log 不支持上下文,所以不能指望它自动读取 context.Value。你需要一个能从 context.Context 提取 ID 的日志封装,或者改用支持 context 的日志库(如 zerolog 或 zap)。
如果坚持用标准 log,最简方案是:在中间件里把 ID 注入到 req.Header(如 X-Request-ID),然后日志函数从 req.Header.Get("X-Request-ID") 取——但这只是“透传”,不是“绑定”,一旦跨 goroutine(比如异步任务)就丢失。
- 推荐做法:封装一个
Logger类型,构造时接收context.Context,内部用ctx.Value()提取 ID 并缓存 - 避免在日志格式中每次都调用
ctx.Value(),高频日志下有性能损耗 - 注意:不要把
context.Context存成全局变量或长期持有,它只应在请求生命周期内有效
并发安全与性能陷阱
uuid.NewString() 本身是并发安全的,但如果你自己实现 ID 生成器(比如基于原子计数器 + 时间戳),就必须加锁或用 atomic。不过不建议这么做——UUID v4 的碰撞概率足够低,且 uuid 包底层已优化过性能。
- 别在 handler 里用
time.Now().UnixNano()拼接 PID 做 ID:单机高并发下 nano 时间可能重复,PID 也可能复用 - 别用
rand.Intn()等非加密随机数生成器做 ID:熵不足,可预测,不符合 trace 场景要求 - 如果用
github.com/google/uuid,确保是 v1.3+,旧版在某些平台有竞态问题
真正的复杂点不在生成,而在透传深度:HTTP → gRPC → DB query → background job,每跳都得显式携带 context,漏一环,ID 就断了。











