Gin 默认日志不能直接替换为Zap,因zap.Logger未实现io.Writer接口;正确做法是禁用内置日志、使用自定义中间件将请求信息喂给Zap,并注意c.Next()位置、Zap配置优化及错误/panic的主动记录。

为什么 Gin 默认日志不能直接替换为 Zap
Gin 的 gin.DefaultWriter 和 gin.DefaultErrorWriter 是写入 io.Writer 的接口,而 Zap 本身不直接实现该接口——zap.Logger 没有 Write([]byte) 方法。强行用 zap.Sugar().Writer() 或包装成 io.Writer 容易丢日志、乱序、或阻塞请求。
真正可行的方式是:禁用 Gin 内置日志中间件,改用自定义中间件把请求信息喂给 Zap。
- 调用
gin.SetMode(gin.ReleaseMode)(避免开发模式下重复打日志) - 用
r.Use(LoggerWithZap(zapLogger))替代gin.Logger() - Zap logger 实例需是全局复用的,不要每次中间件里新建
如何写一个兼容 Gin 上下文的 Zap 中间件
关键在于从 c.Request 和 c.Writer.Status()、c.Writer.Size() 提取字段,并确保在写响应后才记录(否则拿不到状态码和字节数)。
示例中间件逻辑:
func LoggerWithZap(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next() // 必须放中间,等 handler 执行完再取响应信息
duration := time.Since(start)
status := c.Writer.Status()
size := c.Writer.Size()
logger.Info("http request",
zap.String("path", path),
zap.String("query", query),
zap.Int("status", status),
zap.Int("size", size),
zap.Duration("duration", duration),
zap.String("method", c.Request.Method),
zap.String("ip", c.ClientIP()),
)
}
}
-
c.Next()位置不能错,否则Status()和Size()都是 0 - 如果用了
gin.Recovery(),它必须在 Zap 中间件之后注册,否则 panic 日志会漏掉 - 若要结构化记录 query/body,需提前用
c.ShouldBindJSON()或c.Request.Body读取,但注意 Body 只能读一次
Zap 配置要避开 Gin 常见日志陷阱
Gin 请求量大时,Zap 若配置不当会拖慢整个服务。重点看三个参数:
-
zap.AddCaller():开启后每条日志附加文件名和行号,性能下降约 15%~20%,线上建议关掉 -
zap.NewProductionEncoderConfig():时间格式默认是 RFC3339Nano,但某些日志采集器(如 Filebeat)对纳秒级时间解析不稳定,可手动改TimeKey和EncodeTime - 输出目标别用
os.Stdout直接写——应走zapcore.AddSync(&lumberjack.Logger{...})做轮转,否则磁盘满或 inode 耗尽会导致服务卡死
尤其注意:lumberjack 的 MaxAge 和 MaxBackups 必须设值,否则旧日志永不清除。
怎么让 Zap 同时处理 Gin 错误和业务异常
Gin 的 c.Error(err) 只是把错误塞进上下文的 error slice,不会自动打印;Zap 不会感知它。所以得主动捞出来:
- 在中间件末尾加循环:
for _, e := range c.Errors { logger.Error("gin error", zap.Error(e.Err)) } - 业务层抛异常时,统一用
errors.WithStack()(来自github.com/pkg/errors)或fmt.Errorf("%w", err),Zap 的zap.Error()才能展开堆栈 - 如果用了
gin.H{"error": "xxx"}返回 JSON 错误,那只是响应体,Zap 不知道——需要你在c.AbortWithError()之前手动记一条logger.Warn()
别指望 Zap 自动捕获 panic;Gin 的 Recovery() 是独立机制,你得在 Recovery 中调用 logger.Fatal() 并传入 gin.RecoveryFunc。


















