log.SetOutput 无法直接接 *os.File 实现轮转,因其仅转发字节流且不感知文件生命周期;必须使用 lumberjack 等 io.WriteCloser 封装或自行实现带大小检查、加锁、Close/Open 的 Write 方法。

直接用 log.SetOutput 接裸 *os.File 无法实现轮转,因为标准库不感知文件生命周期;必须用封装好的 io.WriteCloser(如 lumberjack.Logger)或自己实现带检查逻辑的 Write 方法。
为什么 log.SetOutput 不能直接接 *os.File 做轮转
日志轮转不是“改文件名”,而是“换写入目标”。log.SetOutput 只负责把字节流扔给一个 io.Writer,它不会主动查大小、不会关旧文件、也不会开新文件。传进去一个固定 *os.File,所有日志就永远只往那个 fd 写——哪怕你后台偷偷 os.Rename,进程里句柄还指着老文件,新日志照样进错位置。
常见错误现象包括:
- 日志文件持续增长不切分,磁盘被撑爆
- 轮转后新日志仍写进已重命名的旧文件(因未更新
log.SetOutput) - 并发写入时多个 goroutine 同时触发
Close()+OpenFile(),导致write: bad file descriptorpanic
用 lumberjack 实现按大小轮转最省心
它是一个轻量、线程安全、无依赖的 io.WriteCloser,专为轮转设计,已被 zap、logrus 等默认集成。配置关键字段必须明确:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
Filename必须是绝对路径(如/var/log/app/app.log),相对路径在 daemon 场景下极易失效 -
MaxSize单位是 MB,但实际阈值是≥ MaxSize * 1024 * 1024字节;写入前才检查,所以最终文件可能略超 -
MaxBackups控制保留几个.1、.2文件,设为0表示不限——生产环境严禁 -
LocalTime: true必开,否则时间戳是 UTC,排查时容易看错日期 -
Compress: true会调用系统gzip命令压缩旧文件,Docker 镜像里得提前装好(apk add gzip或apt-get install gzip)
示例配置:
logger := &lumberjack.Logger{
Filename: "/var/log/app/app.log",
MaxSize: 100, // 100MB
MaxBackups: 7,
MaxAge: 28, // 天
LocalTime: true,
Compress: true,
}
log.SetOutput(logger)
defer logger.Close() // 必须 defer,确保退出前 flush手动实现轮转时最容易踩的坑
自己封装 io.Writer 看似可控,但绕不开几个硬约束:
- 每次
Write(p []byte)前必须调用f.Stat()拿实时大小,不能靠内存计数——缓冲未刷盘时会误判 - 轮转操作(
Close→os.Rename→os.OpenFile)必须加sync.Mutex,且锁要包住整个流程,否则并发写会撞上已关闭的 fd - Windows 下
os.Rename对正在打开的文件失败,必须先Close再 rename - 新文件权限必须显式设为
0644,否则默认0600,运维查日志时无读权限 - 别用
time.Now()在每次写日志时判断是否跨天——高频写入下性能损耗明显;该用time.AfterFunc精准调度零点轮转
按时间轮转该选 rotatelogs 而不是 lumberjack
lumberjack 不支持按 %Y%m%d 格式生成文件名,它的 MaxAge 是清理策略,不是生成策略。若需 app-20260403.log 这种结构,得用 github.com/lestrrat-go/file-rotatelogs:
- 它通过软链接(
WithLinkName)保证app.log始终指向最新文件,运维不用猜今天是几号 - 轮转时机由
WithRotationTime控制,推荐设为24 * time.Hour,配合WithMaxAge自动清理过期文件 - 它本身不处理压缩,旧文件保留为明文,如需压缩得额外起
exec.Command("gzip", "-f", ...)异步执行 - 注意:它的
Write方法不是原子的,高吞吐场景下可能因 rename + open 延迟导致短暂丢日志,比lumberjack更脆弱
真正难的是路径、权限、磁盘满、只读文件系统这些边界情况——它们不会报错,只会让日志静默消失。上线前务必在容器里跑满 24 小时,用 ls -l /var/log/app 和 df -h 交叉验证轮转是否真实发生。


















