go http日志中间件需用自定义responsewriter拦截writeheader和write以准确捕获状态码,结合结构化日志库如zap,按auth→ratelimit→log→recover顺序部署确保日志完整性与准确性。

Go HTTP 中间件里怎么加请求日志
直接用 log.Printf 或 fmt.Printf 打印请求信息是最常见的错误起点——它不带上下文、不区分请求、时间戳格式混乱,且无法动态开关。真正可用的日志中间件必须绑定每个请求的生命周期,输出结构化字段(如 method、path、status、latency),并支持接入 zap、zerolog 等主流日志库。
核心做法是包装 http.Handler,在 ServeHTTP 入口记录开始时间,在调用 next.ServeHTTP 后读取 ResponseWriter 的状态码和响应体大小(需用自定义 responseWriter 包装):
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lw := &loggingResponseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(lw, r)
log.Printf("%s %s %d %v", r.Method, r.URL.Path, lw.statusCode, time.Since(start))
})
}
注意:不能直接从原始 w 读取状态码,因为 WriteHeader 可能没被显式调用,默认是 200;必须拦截 WriteHeader 和 Write 方法。
为什么自定义 responseWriter 必须实现 WriteHeader 和 Write
Go 的 http.ResponseWriter 是接口,但标准库在写响应时只保证调用 WriteHeader(如果未调用则隐式触发),以及 Write。若只包装 Write 而忽略 WriteHeader,会导致状态码始终为默认值 200,哪怕 handler 里写了 w.WriteHeader(404) 也捕获不到。
正确做法是让自定义类型同时满足:
- 嵌入原
http.ResponseWriter,透传未重写的方法 - 重写
WriteHeader(int)并保存statusCode - 重写
Write([]byte) (int, error),首次调用时若未设状态码则补设为 200
否则日志里的 status 字段完全不可信,排查 5xx/4xx 问题时会误判。
用 zap 实现高性能结构化日志中间件要注意什么
直接在中间件里每请求都调用 zap.L().Info() 没问题,但要注意三个关键点:
- 避免重复创建
*zap.Logger:应复用全局 logger 或从context.Context传入,不要每次新建 - 用
logger.With(zap.String("trace_id", traceID))追加请求级字段,而不是拼接字符串 - 延迟计算耗时:用
zap.Duration("latency", time.Since(start)),别提前算好再传string
示例片段:
func zapLoggingMiddleware(logger *zap.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
lw := &loggingResponseWriter{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(lw, r)
logger.Info("http request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.Int("status", lw.statusCode),
zap.Duration("latency", time.Since(start)),
)
})
}
}
漏掉 zap.Duration 直接传字符串,会丢失纳秒精度,且无法被日志分析系统做数值聚合。
中间件顺序错位导致日志漏打或状态码不准
日志中间件必须放在所有可能提前终止请求的中间件之后——比如认证中间件(401)、限流中间件(429)、panic 恢复中间件(500)。如果日志在最外层,而 panic 恢复中间件在内层,那么 panic 发生时日志还没执行,就看不到这条失败请求。
典型错误链:
-
recoveryMiddleware(logMiddleware(authMiddleware(handler)))→ 日志永远打不到 401/429 -
logMiddleware(recoveryMiddleware(authMiddleware(handler)))→ 日志能打,但 panic 后的 500 状态码可能被 recovery 中间件覆盖前已写入,造成不一致
最稳妥顺序是:auth → rateLimit → log → recover → handler。recover 必须在 log 之后,才能确保即使 panic 也能记录完整请求上下文。
真实项目里最容易被忽略的是 recovery 和 log 的相对位置——它不报错,但会让日志缺失关键故障现场。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











