




最常用方式是用 http.Handler 包裹原始 handler 并包装 http.ResponseWriter,记录 remote_addr、method、path、status、duration_ms、user_agent 等字段,需在 WriteHeader/Write 后获取状态码与响应长度,避免直接拼接 RawQuery 或传 *http.Request 给 zap.Any。
用 http.Handler 包裹原始 handler 是最常用也最可控的方式。不依赖第三方框架,就能在请求进入和响应写出前后插入日志逻辑。
WriteHeader 和 Write 被调用后才记录状态码与响应体长度,否则会拿到默认的 200 和 0http.ResponseWriter 实现自定义写入行为,比如用 responseWriter 结构体嵌入原生类型并重写方法remote_addr、method、path、status、duration_ms、user_agent
r.URL.RawQuery,需先用 url.QueryEscape 处理,否则可能破坏日志格式或引发注入风险可以打,但无法获取响应状态码、实际响应字节数、处理耗时等关键指标——因为这些值在 handler 执行结束时还未确定。
log.Printf 放在 handler 开头只能记“开始”,放在结尾只能记“返回”,但不知道最终 WriteHeader(404) 还是 WriteHeader(200)

生产环境建议用 zap 替代 log 标准库,配合中间件做结构化输出。性能高,支持字段附加,方便后续接入 ELK 或 Loki。
zap.NewNop() 初始化 logger,再通过 With(zap.String("trace_id", ...)) 动态加字段r.Context() 取上下文值(如 trace ID),需提前在入口中间件注入,否则为 nilzap.Stringer 类型字段(如自定义 error)要实现 String() string 方法,否则日志里只显示类型名*http.Request 当字段传给 zap.Any,会触发大量反射,拖慢性能HTTP 请求体只能读一次。想记录又不影响业务 handler 解析,必须用 io.TeeReader 或缓存到内存/临时文件。
bytes.NewReader 重建 body,但要注意 Content-Length 头是否同步更新io.LimitReader(r.Body, 1024))json.Decode 或 form.Parse,它们内部已读 body,此时再读会返回空;需确保日志中间件在它们之前执行