必须用 wrapper 包装 http.ResponseWriter 才能获取真实状态码和响应字节数,因原生接口不暴露这些信息;直接 log.Printf(r) 会 panic,因 *http.Request 含未导出字段与循环引用,且 r.Body 为单次读取流,重复读将导致 EOF。

必须用 wrapper 包装 http.ResponseWriter 才能拿到真实状态码,且日志体不能直接读 r.Body —— 否则后续 handler 会收不到数据。
为什么直接 log.Printf(r) 会 panic
Go 的 http.Request 包含大量未导出字段和循环引用,fmt.Printf 或 log.Println 直接打印结构体会触发 runtime panic。更严重的是,r.Body 是单次读取的 io.ReadCloser,一旦中间件调用 io.ReadAll(r.Body),后续 handler 再读就是 EOF,常见报错如 json: cannot unmarshal object into Go value of type xxx。
解决路径只有两条:
- 只记录不读 body 的字段(method、path、remote IP、user agent),但 status 和耗时必须等响应结束才能确定
- 若需记录请求体或 query 参数,必须用
io.TeeReader或bytes.Buffer+io.MultiReader复制流,再重设r.Body - 对
application/x-www-form-urlencoded或multipart/form-data,优先走r.ParseForm()拿r.PostForm,比手动解析更安全
如何包装 ResponseWriter 拦截状态码和字节数
标准 http.ResponseWriter 不暴露 Status() 或已写入字节数,必须实现自定义 wrapper:
立即学习“go语言免费学习笔记(深入)”;
- 定义结构体,内嵌原始
http.ResponseWriter,并加字段:status int(初始 0)、written int、wroteHeader bool -
WriteHeader(statusCode int)中更新status并设wroteHeader = true -
Write(p []byte)中,若!wroteHeader,先调用rw.ResponseWriter.WriteHeader(http.StatusOK),再记status = 200 - 所有写入通过
io.MultiWriter同时写向原始 writer 和一个bytes.Buffer,后者用于统计written
不这样做,c.Writer.Status()(Gin)或自定义 wrapper 的 Status() 方法永远返回 0 或默认值。
用 zap 实现结构化日志输出
避免拼接字符串日志,用 zap.Logger 输出 JSON 更利于 ELK 或 Loki 收集:
- 初始化时用
zap.NewProduction()或zap.NewDevelopment(),不要用zap.L()全局实例(不易控制 level 和输出目标) - 日志字段必须显式传参:
logger.Info("http request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.Int("status", rw.status), zap.Duration("duration", dur)) - 敏感字段(如
password、token)不能靠字符串匹配脱敏,应结合r.Method、r.URL.Path、r.Header.Get("Content-Type")动态解析 body 或 form 后递归替换 key 对应的字符串 value - query 参数脱敏要同步更新
r.URL.RawQuery,否则日志里仍显示明文
Gin 框架中注册与执行时机陷阱
Gin 的中间件签名是 func(*gin.Context),执行顺序直接影响能否拿到完整日志信息:
- 脱敏逻辑(读 body / form / query)必须在
c.Next()之前,否则 body 已被 binding 消费 - 记录状态码、耗时、响应长度必须在
c.Next()之后,否则c.Writer.Status()返回 0 - 日志中间件建议放在最外层(即
engine.Use()最先注册),确保覆盖所有路由;panic 恢复中间件应紧贴其外,避免日志被 panic 中断 - 不要在中间件里调用
c.Abort()后还继续写日志 —— 此时c.Writer.Status()可能未刷新,应统一用 wrapper 的最终status字段
最易被忽略的一点:没做 http.MaxBytesReader 限流,大请求体直接 OOM;脱敏时把非字符串类型的 value(比如数字 token ID)也替换成 "***",破坏了 JSON 结构合法性。


















