必须包装 responsewriter 才能获取真实状态码、响应体大小和耗时,因裸 log.printf 无法捕获 writeheader/write 调用;中间件须为 func(http.handler) http.handler 类型,且需完整重写 responsewriter 方法以确保日志字段准确。

Go 标准库 net/http 本身不提供日志中间件,必须手动封装 http.Handler 并劫持 ResponseWriter 才能拿到状态码、响应体大小和真实耗时——只在 handler 里写 log.Printf 是无效的。
为什么必须包装 ResponseWriter
直接调用 log.Printf 只能记录请求开始前的信息,拿不到响应状态码(w.WriteHeader() 可能被下游 handler 覆盖)、实际写入字节数(w.Write() 返回值)、以及 client 是否断连导致的写失败。真正的日志字段如 status、size、duration_ms 必须靠自定义 ResponseWriter 拦截 WriteHeader 和 Write 方法来捕获。
- 不包装
ResponseWriter→ 日志里status恒为 0,size无法统计,duration包含 panic 后 defer 的虚假时间 - 包装后需重写
WriteHeader(int)记录首次状态码,Write([]byte) (int, error)累加字节数 - 开始计时必须放在 wrapper 创建前(即
time.Now()在闭包外),否则有纳秒级偏差
logging 中间件函数签名和调用链必须严格匹配 http.Handler
中间件不是任意函数,它必须是 func(http.Handler) http.Handler 类型;任何返回 http.HandlerFunc 或接受 http.HandlerFunc 作参数的写法,在嵌套多层时会因类型不匹配而编译失败或运行时卡死。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- ✅ 正确签名:
func logging(next http.Handler) http.Handler - ❌ 错误写法:
func logging(next http.HandlerFunc) http.HandlerFunc—— 后续再套auth()就会类型错配 - 漏掉
next.ServeHTTP(w, r)→ 请求永远不往下传,客户端卡住无响应 - 注册必须用
http.Handle("/path", logging(handler)),不能用http.HandleFunc,后者只收裸函数
记录哪些字段才算生产可用
开发期打个 method + path 够用,但上线后至少要补全五项:状态码、响应字节数、毫秒级耗时、客户端 IP(注意 X-Forwarded-For)、User-Agent。字段来源有差异,不能全靠 r.URL.Path:
-
r.RequestURI记原始路径(含 query),r.URL.Path是解析后路径(已解码、去 query)——日志建议用前者,避免重定向或代理场景下对不上 - 客户端 IP 要从
r.Header.Get("X-Forwarded-For")取首项, fallback 到r.RemoteAddr - 别读
r.Body记请求内容——会消耗 body 导致下游 handler 读不到;真要采样,得用io.TeeReader或仅限小请求做io.ReadAll+r.Body = io.NopCloser(bytes.NewReader(buf)) - 敏感字段(如 token、password)必须过滤,不能原样进日志
性能与采样控制不能忽略
每请求都打结构化日志(比如用 zap)+ 写磁盘,在高并发下会成为瓶颈。生产环境必须加采样开关,且日志写入应异步或批量 flush。
- 默认关闭完整日志,只对
status >= 400或指定 path(如/api/)开启 - 避免在中间件里做 JSON 序列化或格式化——交给日志库(如
lumberjack+zap)统一处理 - 不要用
time.Since在defer里算耗时:handler panic 时defer仍执行,但WriteHeader可能根本没调,状态码记成 0 - 耗时计算应基于两个
time.Time差值,且放在next.ServeHTTP之后、日志输出之前
最易被忽略的是 ResponseWriter 包装的完整性:少重写一个方法(比如漏了 WriteHeader),状态码就永远拿不准;少存一个字段(比如没缓存初始 status),panic 场景下日志就失去诊断价值。中间件看着简单,但每个字节、每个时间点、每个接口实现,都得对得上标准库的行为契约。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










