必须包装 responsewriter,因为原生接口不暴露状态码和写入字节数;需重写 writeheader 和 write 方法捕获真实 statuscode 与 size,并为每个请求创建独立包装实例以保障并发安全。

Go 的日志中间件不是“配出来”的,而是必须通过包装 http.ResponseWriter 和组合 http.Handler 实现的;不这么做,就拿不到真实状态码、响应字节数和准确耗时。
为什么必须包装 ResponseWriter
标准 http.ResponseWriter 是个接口,不暴露 StatusCode() 或已写入字节数。直接在 handler 里用 log.Printf 只能打固定值(比如硬写 200),但实际可能是 404 或 500 —— 甚至 handler panic 后根本没走到日志语句。
-
WriteHeader必须被拦截:用于捕获显式设置的状态码 -
Write必须被重写:用于累加写入字节数,并 fallback 到200(当 handler 没调WriteHeader就直接Write时) - 每个请求需独立实例:不能复用结构体字段,否则并发下
status和size会错乱
logging 中间件函数签名怎么写才可链式组合
签名必须统一为 func(http.Handler) http.Handler,否则嵌套时类型不匹配。常见错误是写成 func(http.HandlerFunc) http.HandlerFunc,它在单层看似能跑,但套两层就编译失败。
- 返回值必须是
http.Handler,不是http.HandlerFunc(尽管后者实现了接口,但 Go 不自动类型转换) - 内部用
http.HandlerFunc包装逻辑,再返回,这是最稳妥的构造方式 - 注册时必须用
http.Handle或srvMux.Handle,不能用http.HandleFunc—— 后者只接受裸函数,不接受已包装的中间件
记录哪些字段才算真正可用的日志
只记 method 和 path 是半残废日志;生产环境至少要包含五项:耗时、状态码、响应体大小、客户端 IP、原始请求路径(r.RequestURI 比 r.URL.Path 更可靠)。
- 耗时用
time.Since(start),别用Format字符串拼接,避免格式化开销 - 状态码必须来自包装器的
status字段,不是硬编码或r.URL解析结果 - 敏感参数(如
?token=xxx)需脱敏,否则日志可能泄露凭证 - 如果用了
gorilla/mux,可选加mux.CurrentRoute(r).GetName(),但得包recover防 panic
要不要读 r.Body?怎么读才安全
除非业务强依赖原始 payload(比如审计、重放),否则不要读 r.Body。一读就 EOF,下游 handler 拿不到数据,典型现象是 json: cannot unmarshal object into Go value。
- 真要读,必须用
io.ReadAll+bytes.NewReader重建 body,并赋回r.Body - 重建后记得用
io.NopCloser包一层,否则Close被忽略,可能泄漏资源 - 更轻量的做法是用
httputil.DumpRequestOut(仅限出站调试),或只取r.Header.Get("Content-Length")做粗略估算
最容易被跳过的其实是采样和上下文透传:高频请求全打日志会拖慢服务,而跨中间件传递 traceID 不做 context.WithValue,日志就断链。这两点不处理,日志再全也没法关联问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











