Go标准库log包不支持日志轮转,需用lumberjack等第三方库;os.OpenFile+SetOutput仅重定向输出,无法解决大小/时间轮转、并发安全及重启续号问题。

Go 标准库的 log 包本身不支持日志轮转,直接写文件会无限追加、撑爆磁盘。必须借助第三方库或手动封装,否则生产环境无法接受。
为什么不能只用 os.OpenFile + log.SetOutput
很多人尝试用 os.OpenFile 打开一个文件,再传给 log.SetOutput,以为能“接管”日志输出——但这只是把日志重定向到文件,完全没解决轮转问题。轮转需要判断文件大小/时间、重命名旧文件、创建新文件、甚至压缩归档,标准库不做这些事。
- 没有自动切分逻辑:不会在达到 100MB 时自动 rename 成
app.log.2024-05-20.001 - 并发写入不安全:多个 goroutine 同时写同一个文件句柄,可能丢日志或损坏内容(即使加锁,也影响性能)
- 进程重启后无法续接轮转计数:比如上次是
app.log.003,重启后又从.001开始,覆盖旧日志
推荐方案:用 lumberjack 封装 log.SetOutput
lumberjack 是最成熟、轻量、被大量生产项目验证的日志轮转库(如 Kubernetes、Docker 的部分组件也在用)。它实现的是 io.WriteCloser 接口,可直接塞进 log.SetOutput,无需改日志调用方式。
安装:
go get gopkg.in/natefinch/lumberjack.v2
立即学习“go语言免费学习笔记(深入)”;
基础用法示例:
package main
import (
"log"
"gopkg.in/natefinch/lumberjack.v2"
)
func main() {
logger := log.New(&lumberjack.Logger{
Filename: "/var/log/myapp/app.log",
MaxSize: 100, // MB
MaxBackups: 7,
MaxAge: 28, // 天
Compress: true,
}, "", log.LstdFlags)
logger.Println("服务启动")
}
-
MaxSize单位是 MB,不是字节;超过即触发轮转 -
MaxBackups控制保留几个历史文件(.001~.007),超出的自动删除 -
MaxAge是按文件最后修改时间计算,和当前系统时间比对,不是日志内容里的时间戳 - 压缩仅支持 gzip,且只对归档后的
.gz文件生效,主日志文件(app.log)始终是明文
自定义轮转需注意的底层细节
如果因合规或审计要求必须自己实现(比如要加密归档、对接 S3、打特定 trace ID 前缀),绕不开这几个关键点:
- 轮转判定必须原子:先
os.Stat检查大小,再os.Rename,中间不能有其他写入,否则可能丢失日志。建议用sync.Mutex+ 双缓冲或 channel 控制写入队列 - 文件名生成要防冲突:不要只靠
time.Now().Format("20060102"),同一秒内多次轮转会覆盖。应加上序号或纳秒级时间戳,如app.log.20240520.123456789 - 旧文件清理不能用
filepath.Glob遍历全目录:当磁盘 IO 压力大时,Glob可能阻塞主线程。应维护一个内存中的文件名列表,轮转时更新 -
os.Chmod和os.Chown要在OpenFile后立即调用,否则新建文件可能继承错误权限(尤其容器里 uid/gid 不匹配时)
轮转真正的难点不在“怎么切”,而在“切得稳、不丢、不卡、不冲突”。哪怕只用 lumberjack,也要实测压测场景下它的锁竞争和 GC 表现——有些版本在高并发小日志(每毫秒百条)下,Write 方法会成为瓶颈。










