应在最外层中间件用 uuid.NewString() 生成 request_id,并通过 context.WithValue 注入 r.Context(),再调用 next.ServeHTTP(w, r.WithContext(ctx));key 推荐自定义类型,日志需从 ctx 提取并注入 zap/zerolog 等。

怎么在 HTTP 请求入口生成唯一 request_id
Go 的标准 http.ServeMux 或 http.Handler 链中,必须在最外层中间件里生成并注入 request_id,否则下游日志无法统一关联。推荐用 uuid.NewString()(Go 1.20+)或 github.com/google/uuid 的 uuid.New().String(),避免用时间戳+随机数拼接——容易碰撞且不满足分布式唯一性。
关键点是把 request_id 塞进 context.Context,而不是存在全局变量或 struct 字段里:
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := uuid.NewString()
ctx := context.WithValue(r.Context(), "request_id", reqID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
注意:context.WithValue 的 key 类型建议用自定义类型(如 type requestIDKey struct{}),避免字符串 key 冲突;但若只是内部小项目,字符串 key 可接受,别用 "request_id" 这种泛用名,改用 "myapp.request_id" 更安全。
怎么让日志库自动带上当前请求的 request_id
主流日志库(zap、logrus、zerolog)都支持 context 携带字段,但不是默认开启的。以 zap 为例,不能只靠 zap.String("request_id", ...) 手动传——容易漏、难维护。正确做法是封装一个带上下文感知的日志函数:
- 用
zap.AddCallerSkip(1)避免日志行号跳到 wrapper 层 - 从
r.Context()或ctx.Value()提取request_id,没提取到就 fallback 到"N/A",别 panic - 每次调用
logger.With(...)构造新实例,而不是复用全局 logger
示例(zap):
func LogWithRequestID(ctx context.Context, logger *zap.Logger) *zap.Logger {
if reqID, ok := ctx.Value("myapp.request_id").(string); ok {
return logger.With(zap.String("request_id", reqID))
}
return logger.With(zap.String("request_id", "N/A"))
}
// 在 handler 中:
func myHandler(w http.ResponseWriter, r *http.Request) {
logger := LogWithRequestID(r.Context(), globalLogger)
logger.Info("handling request", zap.String("path", r.URL.Path))
}
为什么用 context.Context 而不是 http.Request.Header 传 request_id
Header 只能用于跨服务传递(比如通过 X-Request-ID),但本机日志关联不需要走网络层。如果只依赖 Header:
- 本地调试时 Header 可能为空,导致日志丢失
request_id - 中间件顺序错位(比如鉴权中间件在 request_id 之前执行),会导致其内部日志拿不到 ID
- 非 HTTP 场景(如定时任务、消息消费)没法复用同一套日志逻辑
而 context.Context 是 Go 原生的、跨 goroutine 安全的传递机制,所有异步操作(go func() { ... }())、子调用、数据库查询都能自然继承。只要你在入口处塞一次,后续所有 ctx 衍生链路都自带 ID。
常见日志追踪失效的三个坑
实际部署后发现日志没 ID?大概率掉进这几个坑:
-
goroutine启动时没显式传ctx:写成go doWork()而不是go doWork(ctx),子 goroutine 用的是空 context - 第三方库(如
sqlx、redis-go)的日志钩子没接入 context,需手动 wrap 或关掉它们的默认日志 - 日志输出格式没包含
request_id字段:检查zap.Config.EncoderConfig是否启用了request_id对应的 key,或logrus.Formatter是否把Fields正确渲染了
最隐蔽的问题是:某些中间件(比如 prometheus 的 metrics middleware)会替换 *http.Request 实例,导致 context 断链。遇到这种情况,得在它之后再补一层 request_id 注入,或者改用 net/http 的 HandlerFunc 包装顺序来规避。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











