不能只靠 uuid.new() 生成请求id,因其与请求生命周期脱钩,导致中间件、日志及下游调用无法自动获取;必须通过中间件在入口统一生成并注入 context.context,再经 r.withcontext() 和 header(如 x-request-id)透传,确保整个请求链路 id 一致。

为什么不能只靠 uuid.New() 生成请求ID
直接在每个 handler 里调用 uuid.New() 看似简单,但会导致同一请求经过多个中间件或下游服务时 ID 不一致。HTTP 请求生命周期中,一次入口调用可能触发多次 goroutine(比如并发调用依赖服务),若每个 goroutine 都独立生成 ID,链路就断了。
真正需要的是“请求上下文绑定”的 ID —— 从入口开始注入,后续所有子 goroutine、日志、HTTP 调用都复用同一个值。Go 的 context.Context 正是为此设计的载体。
- 必须在最外层(如 HTTP middleware)生成并注入到
context.Context,而非 handler 内部 - 下游 HTTP 调用需通过
req.WithContext()透传该 context,否则新 goroutine 拿不到父级 ID - 避免用全局变量或
goroutine local storage替代 context,它们无法跨 goroutine 安全传递
如何用 middleware 统一注入请求ID并透传
标准做法是在 HTTP server 的中间件中生成 ID,并写入 request context;同时确保下游 HTTP 请求携带该 ID 作为 header(如 X-Request-ID)。
示例中间件:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.New().String()
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), "request_id", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
- 务必用
r.WithContext()构造新 *http.Request,原 request 是不可变的 - 不要用
context.WithValue()存 struct 或 interface{},只存 string 等基础类型,避免内存泄漏和类型断言错误 - 如果用了 Gin/Echo 等框架,优先使用其内置 context(如
c.Request.Context()),再 wrap
下游服务调用时怎么把 ID 透传过去
Go 的 http.Client 默认不继承父 context 的值,必须显式把 ID 从当前 context 取出,塞进下游 request header。
典型错误写法:http.NewRequest("GET", url, nil) —— 完全丢失 context 和 ID。
- 正确方式:用
http.NewRequestWithContext(ctx, method, url, body),然后手动设置 header - 推荐封装一个带透传逻辑的 client 方法,例如:
DoWithTrace(ctx, req),内部自动读取ctx.Value("request_id")并设为X-Request-ID - 注意:gRPC 场景下要用
metadata.MD{"x-request-id": []string{id}}+grpc.MetadataCarrier注入 context
日志里怎么自动带上请求ID而不每处都手动传
直接在 log 语句里拼接 id + "msg" 很容易漏,且破坏结构化日志格式。理想方案是 logger 实例能从 context 中动态提取 ID。
以 zap 为例,可封装一个带 context 提取能力的 Logger:
func (l *Logger) WithCtx(ctx context.Context) *zap.Logger {
if id := ctx.Value("request_id"); id != nil {
return l.logger.With(zap.String("req_id", id.(string)))
}
return l.logger
}
- 不要在全局 logger 上直接
With(),那会污染所有日志;必须每次基于 context 动态生成 - 若用
log/slog(Go 1.21+),可通过slog.WithGroup("req").With("id", id)实现类似效果,但 group 名需与 context 解析逻辑对齐 - 警惕日志库的
WithValues在高并发下分配对象的开销,string 类型 ID 影响小,但 map 或 struct 就要谨慎
go func() { ... }() 里没传 context,或者用了 time.AfterFunc 这类不接受 context 的 API。这时候 ID 就彻底丢失了,连日志都对不上。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










