Gin 默认日志与 Zap 不能共存,因 Gin 的 DefaultWriter/DefaultErrorWriter 要求 io.Writer 接口,而 Zap 的 *zap.Logger 不实现该接口,强行替换会 panic 或日志丢失;必须通过自定义中间件或 gin.WithCustomLogger(v1.9+)接管日志逻辑。

为什么 Gin 默认日志和 Zap 不能共存
Gin 的 gin.DefaultWriter 和 gin.DefaultErrorWriter 是底层 io.Writer 接口,而 Zap 的 zap.Logger 不直接实现该接口——它输出的是结构化日志,不是纯文本流。强行替换会导致日志丢失、格式错乱或 panic。
常见错误现象:panic: interface conversion: *zap.Logger is not io.Writer;或者控制台看不到请求日志,但 Zap 文件里只有 error 级别内容。
- 必须用
gin.WithCustomLogger(v1.9+)或中间件接管日志写入逻辑 - Zap 的
Sync()必须在程序退出前显式调用,否则最后一段日志可能不落盘 - 不要把
zap.NewExample()用于生产——它默认只输出到 stdout 且无缓冲、无轮转
如何让 Gin 请求日志进 Zap(含耗时、状态码、路径)
核心是写一个中间件,用 gin.HandlerFunc 拦截请求,在 ctx.Next() 前后采集时间、状态码、路径等字段,再调用 logger.Info 或 logger.Warn 输出结构化记录。
示例关键片段:
立即学习“go语言免费学习笔记(深入)”;
func ZapLogger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
latency := time.Since(start)
statusCode := c.Writer.Status()
clientIP := c.ClientIP()
fields := []zap.Field{
zap.Int("status", statusCode),
zap.String("path", path),
zap.String("method", c.Request.Method),
zap.String("ip", clientIP),
zap.Duration("latency", latency),
}
if len(c.Errors) > 0 {
logger.Warn("request completed with errors", fields...)
} else {
logger.Info("request completed", fields...)
}
}
}
- 注意:不要在
c.Next()前就调用logger.Info,否则拿不到最终状态码和耗时 - 若需记录请求体或响应体,需用
c.Request.Body重放(有性能代价),且要判断c.Request.ContentLength防止大包阻塞 - 避免在字段中传入未清洗的
c.Request.UserAgent()或c.GetHeader("X-Forwarded-For"),防止日志注入
Zap 生产配置绕不开的三个参数
不用 zap.NewProductionConfig() 直接开跑,否则会因默认设置导致磁盘打满或日志不可读。
-
EncoderConfig.EncodeTime:必须设为zapcore.ISO8601TimeEncoder或自定义 UnixMs,否则时间字段是空接口,ES/Kibana 解析失败 -
EncoderConfig.EncodeLevel:推荐zapcore.CapitalLevelEncoder,小写 level 在某些日志平台里不被识别为标准字段 -
WriteSyncer要套lumberjack.Logger做轮转:zapcore.AddSync(&lumberjack.Logger{Filename: "logs/app.log", MaxSize: 100, MaxBackups: 7});MaxSize单位是 MB,不是字节
漏掉任意一项,都可能导致日志时间戳无法排序、级别字段缺失、或单个日志文件涨到几十 GB 占满磁盘。
panic 时怎么确保 Zap 日志不丢(尤其是 defer 场景)
Gin 的 recovery 中间件默认用 log.Printf,和 Zap 完全无关。一旦发生 panic,Zap 缓冲区里的日志若没同步,就永远消失了。
- 必须在 recovery 中间件里手动调用
logger.Sync(),且放在recover()后、panic()前 - 建议封装一个带 Sync 的 error logger:
logger.With(zap.String("source", "panic-recovery")).Fatal("panic recovered", zap.String("stack", stack)) - 如果用了
defer logger.Sync(),要确保它在 main 函数最外层,且在router.Run()之后——否则服务进程退出太快,Sync 来不及刷盘
线上最常忽略的是 Sync 时机:Zap 默认异步写日志,panic 发生时 goroutine 可能已销毁,缓冲区还在内存里,没人来 flush 它。


















