最常用方式是用 http.Handler 包裹原始 handler 并包装 http.ResponseWriter,记录 remote_addr、method、path、status、duration_ms、user_agent 等字段,需在 WriteHeader/Write 后获取状态码与响应长度,避免直接拼接 RawQuery 或传 *http.Request 给 zap.Any。

Go HTTP 中间件如何统一记录访问日志
用 http.Handler 包裹原始 handler 是最常用也最可控的方式。不依赖第三方框架,就能在请求进入和响应写出前后插入日志逻辑。
- 必须在
WriteHeader和Write被调用后才记录状态码与响应体长度,否则会拿到默认的 200 和 0 - 推荐包装
http.ResponseWriter实现自定义写入行为,比如用responseWriter结构体嵌入原生类型并重写方法 - 日志字段建议至少包含:
remote_addr、method、path、status、duration_ms、user_agent - 避免在日志中直接拼接
r.URL.RawQuery,需先用url.QueryEscape处理,否则可能破坏日志格式或引发注入风险
为什么不能直接用 log.Print 在 handler 里打日志
可以打,但无法获取响应状态码、实际响应字节数、处理耗时等关键指标——因为这些值在 handler 执行结束时还未确定。
-
log.Printf放在 handler 开头只能记“开始”,放在结尾只能记“返回”,但不知道最终WriteHeader(404)还是WriteHeader(200) - 如果 handler panic,且没被 recover,日志甚至不会走到结尾,导致“有请求无日志”现象
- 多个 handler 共用同一份日志逻辑时,重复写日志语句易出错,也不利于集中配置(如开关、采样率、输出目标)
使用三方库 zap + middleware 实现结构化日志
生产环境建议用 zap 替代 log 标准库,配合中间件做结构化输出。性能高,支持字段附加,方便后续接入 ELK 或 Loki。
- 用
zap.NewNop()初始化 logger,再通过With(zap.String("trace_id", ...))动态加字段 - 从
r.Context()取上下文值(如 trace ID),需提前在入口中间件注入,否则为 nil - 注意
zap.Stringer类型字段(如自定义 error)要实现String() string方法,否则日志里只显示类型名 - 避免把整个
*http.Request当字段传给zap.Any,会触发大量反射,拖慢性能
如何安全记录请求体(Body)而不影响后续读取
HTTP 请求体只能读一次。想记录又不影响业务 handler 解析,必须用 io.TeeReader 或缓存到内存/临时文件。
立即学习“go语言免费学习笔记(深入)”;
- 小请求(如 JSON API,io.ReadAll(r.Body) +
bytes.NewReader重建 body,但要注意 Content-Length 头是否同步更新 - 大文件上传场景禁止读全部 body,应跳过记录或仅记录前 N 字节(
io.LimitReader(r.Body, 1024)) - 若用了
json.Decode或form.Parse,它们内部已读 body,此时再读会返回空;需确保日志中间件在它们之前执行 - 敏感字段(如 password、token)必须在记录前过滤,建议用正则或键名匹配方式脱敏,而不是靠业务层“约定不传”










