zap 默认 core 不支持滚动写,因其仅同步写入 writesyncer,而 os.file 等无轮转能力;必须用 *lumberjack.logger(取地址)包装为 zapcore.writesyncer,并配置 maxsize(mb)、maxbackups、maxage 才能实现滚动。

为什么默认的 Zap Core 不支持滚动写
Zap 默认的 zapcore.Core 是无状态、无缓冲、同步写入的,它只管把日志条目交给 WriteSyncer,而标准的 os.File 或 os.Stdout 都不自带滚动逻辑。直接用 zap.New(zapcore.NewCore(...)) 搭配普通文件写入器,日志会一直追加,永远不会切分或归档。
要实现滚动写,必须替换掉底层的 WriteSyncer,换成支持轮转(rotation)的实现——最常用的是 lumberjack.Logger,但它本身不是 zapcore.WriteSyncer,需要包装一层。
- 别直接把
lumberjack.Logger传给zapcore.AddSync:它缺少Sync()方法,会 panic 报错"lumberjack.Logger does not implement zapcore.WriteSyncer" - 正确做法是用
zapcore.AddSync(&lumberjack.Logger{...})—— 注意取地址,因为*lumberjack.Logger才实现了WriteSyncer -
lumberjack的MaxSize单位是 MB(不是字节),MaxBackups是保留旧日志文件个数,MaxAge是按天算,三者共同触发轮转,但优先级:大小满 > 天数超 > 备份数超
如何启用异步写但不丢日志
Zap 的异步能力来自 zapcore.NewTee 或更常用的 zap.WrapCore + zapcore.Lock + 自定义缓冲队列,但官方推荐且最稳妥的方式是用 zap.NewAsync 包装整个 logger —— 它内部用无锁环形缓冲区 + 协程消费,性能高,且默认带崩溃保护。
关键点在于:异步模式下,logger.Info 等调用立即返回,真正写入由后台 goroutine 执行;如果程序提前退出(比如 os.Exit(0)),缓冲区里的日志会丢失。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须在 main 函数退出前显式调用
logger.Sync(),否则最后几条日志大概率不落盘 -
zap.NewAsync返回的是*zap.Logger,但它的Sync()方法是阻塞的,会等所有待写日志 flush 完才返回 - 不要对每个日志都调用
Sync(),只在进程退出时调一次;高频写场景下,可配合signal.Notify捕获SIGINT/SIGTERM后优雅关闭 - 异步缓冲区默认大小是 128KB(约数千条日志),可通过
zap.BufferPool替换为更大池子,但一般无需改动
滚动 + 异步组合配置的典型代码片段
下面这段是生产环境可用的最小可行配置,兼顾滚动、异步、结构化输出和错误防护:
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
func NewLogger(logFile string) (*zap.Logger, error) {
lumberJackLogger := &lumberjack.Logger{
Filename: logFile,
MaxSize: 100, // MB
MaxBackups: 5,
MaxAge: 28, // days
Compress: true,
}
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
core := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
zapcore.AddSync(lumberJackLogger), // 注意:必须是指针
zapcore.InfoLevel,
)
logger := zap.New(core, zap.AddCaller(), zap.WithCallerSkip(1))
return zap.NewAsync(logger), nil // 包一层异步 wrapper
}
注意:zap.NewAsync 必须套在已配置好输出目标的 logger 上,不能先异步再设 WriteSyncer;否则 WriteSyncer 会被异步层忽略。
容易被忽略的权限与路径问题
滚动日志依赖文件系统操作,但在容器或受限用户下容易失败,错误通常不报在日志里,而是静默降级到 stderr 或 panic。
-
lumberjack创建新文件时用的是os.O_CREATE | os.O_WRONLY | os.O_APPEND,要求父目录存在且当前用户有写权限;若目录不存在,轮转会失败,但不会报错,只会持续往一个不可写的文件句柄写,最终write: bad file descriptor - 务必在启动时检查并创建日志目录:
os.MkdirAll(filepath.Dir(logFile), 0755) - 如果用相对路径(如
"logs/app.log"),Zap 启动位置决定实际路径,建议一律用绝对路径,或基于os.Executable计算根目录 - Windows 下
lumberjack对文件锁处理较弱,多进程写同一日志文件仍可能冲突;Linux/macOS 更稳定,但也要避免多个 Zap 实例共用同一个lumberjack.Logger实例
滚动和异步本身不难配,难的是让它们在各种部署环境下安静地跑下去——路径、权限、进程生命周期,才是线上日志沉默失效的真正原因。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










