Gin默认日志中间件无法打印请求体和响应体,因c.Request.Body为单次读取流且ResponseWriter被封装;需重置Body或包装ResponseWriter,并按Cors→Auth→Logger→Recovery顺序注册中间件。

为什么 Gin 默认日志中间件打不出请求体和响应体
因为 c.Request.Body 是单次读取的 io.ReadCloser 流,日志中间件一读就空了,后续业务代码调用 c.ShouldBindJSON() 会报 invalid character;而 http.ResponseWriter 被 Gin 封装过,直接读会 panic 或丢内容。
- 别在日志中间件里调用
c.ShouldBindJSON()或c.PostForm—— 它们会消费 Body - 要记录请求体,得先用
ioutil.ReadAll(c.Request.Body)拿到原始字节,再用bytes.NewBuffer(bodyBytes)和ioutil.NopCloser把 Body 重置回去 - 记录响应体必须包装
ResponseWriter(比如自定义responseWriter结构体),拦截Write()和WriteHeader(),并在c.Next()后读缓冲区 - 生产环境建议只开 debug 模式才记录 body:
if gin.Mode() == gin.DebugMode
怎么用 Zap 替换默认 logger 实现高性能结构化日志
默认 gin.Logger() 只输出文本,不支持字段、级别过滤、日志切割,Zap 的 SugaredLogger 更适合开发调试,Logger 更适合高吞吐服务。
- 安装:
go get -u go.uber.org/zap - 初始化时选对类型:性能敏感用
zap.NewProduction(),开发调试用logger.Sugar() - 中间件中记录字段必须显式传参:
logger.Info("request", zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status())) - 别忘了
defer logger.Sync(),否则进程退出时缓冲区日志会丢失
日志中间件顺序放错会导致 panic 不留痕迹或鉴权失效
中间件执行是链式入栈 + 出栈返回,注册顺序决定逻辑依赖关系。日志中间件如果放在 Recovery 前面,panic 发生时日志根本没刷出来;如果放在鉴权后面,未授权请求也会被记日志,还可能因 c.Get("user") 返回 nil 导致 panic。
- 正确顺序(从左到右):
Cors()→Auth()→Logger()→Recovery() -
Auth中必须用c.Set("user", user)显式存值,下游用user, ok := c.Get("user")判断,不能只靠非空断言 - 避免全局
r.Use(),改用路由组控制范围:api := r.Group("/api").Use(auth, logger)
怎么写一个带耗时统计和 IP 标识的轻量日志中间件
不需要引入 Zap 也能快速写出实用日志,关键是把时间戳、状态码、路径、IP、耗时这五项打全,并控制好 body 读取时机。
立即学习“go语言免费学习笔记(深入)”;
- 用
c.ClientIP()拿真实 IP(注意配置TrustedProxies防伪造) - 计时用
start := time.Now()+c.Next()+cost := time.Since(start) - 状态码必须从
c.Writer.Status()读(不是c.Writer.Written()),因为c.AbortWithStatus()不会触发 WriteHeader - 简单示例:
func Logger() gin.HandlerFunc {<br> return func(c *gin.Context) {<br> start := time.Now()<br> c.Next()<br> cost := time.Since(start)<br> log.Printf("[GIN] %s %s %d %v %s",<br> c.Request.Method,<br> c.Request.URL.Path,<br> c.Writer.Status(),<br> cost,<br> c.ClientIP())<br> }<br>}
c.Writer.Status() 的时机判断和 ClientIP() 的可信代理配置,这两处一错,日志就失去可观测性。


















