Go日志中间件必须包装http.Handler并用statusRecorder包装ResponseWriter,才能准确捕获状态码、响应大小和耗时;需在next.ServeHTTP后记录日志,前置注入trace ID并recover panic,结合slog实现分级脱敏。

Go 日志中间件必须包装 http.Handler,不能只在某个 http.HandleFunc 里写日志——否则漏记、无法捕获 panic、拿不到真实状态码。
为什么必须用 statusRecorder 包装 ResponseWriter
直接读 w.WriteHeader() 不可靠:业务 handler 可能不显式调用,Go 默认返回 200,但你无法在 next.ServeHTTP() 返回后拿到这个值。只有用包装器劫持 WriteHeader 和 Write,才能确保状态码和响应体大小被准确捕获。
-
WriteHeader中缓存status字段 -
Write中累加size,并兜底设status = http.StatusOK(当未显式写头时) - 日志语句必须放在
next.ServeHTTP(rw, r)之后,此时耗时、状态码、大小才全部确定
如何让日志带 trace ID 和 panic 恢复
trace ID 必须在日志中间件执行前就注入 context,否则 r.Context().Value("trace_id") 是 nil;而 panic 若不 recover,next.ServeHTTP() 后的日志语句根本不会执行——整条请求日志就丢了。
- trace ID 应由上游中间件(如
TraceMiddleware)注入,日志层只负责读取:ctx.Value(keyTraceID) - 必须加
defer func() { if r := recover(); r != nil { log.ErrorContext(ctx, "panic recovered", "panic", r) } }() - 避免在中间件里读
r.Body——会消耗流;如需记录 body,先用io.TeeReader复制到 buffer
怎样用 slog 实现分级与脱敏
用 log/slog(Go 1.21+ 内置)比原生 log 更适合接口日志:它天然支持属性、层级、分组,且可通过 slog.HandlerOptions.Level 控制输出粒度。
立即学习“go语言免费学习笔记(深入)”;
- 开发环境设为
slog.LevelDebug,记录完整Header和Query;生产环境设为slog.LevelInfo,只留关键字段 - 敏感字段(如
Authorization、password)统一脱敏为[REDACTED] - 高频接口(如
/healthz)应跳过 debug 日志,可用middleware.Skipper白名单过滤
最易被忽略的是中间件顺序:日志必须在 TraceMiddleware 之后、CircuitBreaker 之后、handler 之前——早了拿不到 trace ID,晚了可能根本没机会执行。另外,statusRecorder 的 Write 方法里那个 if r.status == 0 判断,漏掉就会导致所有未显式写状态码的请求都记成 0,查问题时完全不可信。


















