默认 gin.logger() 不够用,因它仅记录status、method、path等基础信息,不记录请求体、响应体及错误详情,也无法捕获panic堆栈。

直接用 gin.Logger() 就能开箱即用,但默认不记录请求体、响应体和错误详情——真要调试或审计,得自己补全。
为什么默认 gin.Logger() 不够用
它只输出基础信息:status、method、path、latency、ip,连 Content-Type 都不打。遇到 400 或 500,你根本不知道客户端发了啥、服务端返回了啥。更麻烦的是,它不捕获 panic 后的堆栈,线上出错只能靠猜。
常见错误现象:POST /api/v1/user 400 12ms —— 看不出是 JSON 格式错、字段缺失,还是结构体绑定失败。
- 默认中间件不读取
c.Request.Body(读完就不可重放) - 不写入
c.Errors中的校验错误(比如binding失败) - 不兼容
gin.Recovery()的 panic 日志格式,容易漏掉关键上下文
手动实现带请求/响应体的日志中间件
核心是用 c.Request.Body 和 responseWriter 包装器截获数据。注意:必须在其他中间件(如 binding)之前注册,否则 body 已被读空。
实操建议:
- 用
io.TeeReader把请求体复制进bytes.Buffer,再塞回c.Request.Body - 自定义
responseWriter实现http.ResponseWriter接口,缓存WriteHeader和Write结果 - 只对特定
Content-Type(如application/json)记录 body,避免日志爆炸 - 加长度限制(比如
maxBodySize: 1024),防止大文件上传撑爆内存
示例关键逻辑:
func LoggerWithBody() gin.HandlerFunc {
return func(c *gin.Context) {
var bodyBytes []byte
if c.Request.Body != nil {
bodyBytes, _ = io.ReadAll(c.Request.Body)
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// 包装 responseWriter
rw := &responseWriter{ResponseWriter: c.Writer, statusCode: 200}
c.Writer = rw
c.Next()
// 打日志:method、path、status、body(截断)、error(如果有)
if len(bodyBytes) > 0 && len(bodyBytes) 0 && len(rw.body) 0 {
log.Printf("[ERR] %v", c.Errors.ByType(gin.ErrorTypeBind))
}
}
}
如何安全地集成 gin.Recovery() 和自定义日志
默认 gin.Recovery() 会提前结束请求,导致你的日志中间件拿不到最终状态码和响应体。必须确保 Recovery 在日志之后注册,且日志中间件能感知 panic。
实操建议:
- 把自定义日志中间件放在
engine.Use()最前面,gin.Recovery()放最后 - 在日志中间件里用
defer捕获 panic,调用log.Printf("%+v", debug.Stack()) - 别依赖
c.Writer.Status()—— panic 时它可能还是 200,要用包装器里缓存的statusCode - 避免在 Recovery 里重复打印 stack,否则日志重复两遍
真正难的不是写日志,是控制粒度:哪些接口记 body,哪些只记摘要;错误日志要不要打用户 ID;日志要不要异步刷盘。这些不提前想清楚,上线后要么查不到线索,要么磁盘被日志写满。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











