Go日志中间件的核心是实现http.Handler包装器,必须包装ResponseWriter以捕获真实状态码和字节数,用r.RequestURI获取原始路径,禁用JSON Unicode转义需自定义MarshalJSON或换go-json库,时间戳应使用time.Now().UnixMicro()避免格式化开销。

Go日志中间件的核心不是“配置”,而是实现一个符合 http.Handler 接口的包装器,关键在捕获真实状态码、原始路径和避免日志结构体被错误序列化。
为什么直接用 log.Printf 在 handler 里打日志会丢状态码
因为 http.ResponseWriter 的 WriteHeader 调用是幂等且延迟生效的;如果不在中间件里包装它,你根本拿不到最终写出去的状态码——log.Printf 执行时,响应可能还没发出,w.WriteHeader(200) 还没调,甚至可能被后续 panic 或重定向覆盖。
- 必须自己实现一个
loggingResponseWriter类型,嵌入原ResponseWriter并重写WriteHeader和Write -
WriteHeader中缓存code,Write中首次写入时若未调过WriteHeader,则默认设为200 - 别用
r.URL.Path记录路径——反向代理后它常是/;改用r.RequestURI(但记得脱敏敏感参数)
JSON 日志里中文变 \u4f60\u597d 怎么办
这是 json.Marshal 默认行为,不是 bug,但查日志极其痛苦。标准库不提供一键禁用 Unicode 转义的开关,得绕过去。
- 用
json.Encoder替代json.Marshal,调enc.SetEscapeHTML(false)(仅防<code>>被转义) - 要彻底禁用 Unicode 转义,得自己实现
MarshalJSON方法,或换go-json库(它默认不转义非 ASCII) - 别给日志结构体字段加
json:",string"标签——数字会被引号包住,ES/Kibana 识别成 keyword 而非 number 类型
时间戳和性能开销怎么控制
高频日志中,time.Now().Format(...) 是明显瓶颈,字符串拼接 + 格式化占 CPU 很多;微秒级精度又容易被忽略。
立即学习“go语言免费学习笔记(深入)”;
- 用
time.Now().UnixMicro()直接拿整数时间戳,零分配、无格式化开销 - 耗时用
time.Since(start),别手动算纳秒再除——Go 运行时对Since有优化 - 非关键字段(比如完整请求体、header 全量)建议异步采样,避免阻塞主请求链路
- 如果用了
zap,优先用zap.Stringer+ 自定义String()方法,比 JSON 序列化快一个数量级
最易被忽略的一点:所有中间件都依赖 r.Body 读取时的可重放性。如果上游中间件(比如鉴权层)已调用 r.ParseForm() 或 io.ReadAll(r.Body),后续日志中间件再读就会得到空内容——必须在最早一层就用 httputil.DumpRequestOut 或手动 io.ReadFull + r.Body = io.NopCloser 重置,否则请求体日志永远为空。


















