最稳妥方案是用lumberjack.logger实现按大小轮转:设maxsize(mb)、maxbackups=1、maxage=0,传实例给log.setoutput;它不覆盖而切分,但磁盘占用稳定,等效环形日志。

用 lumberjack.Logger 实现自动轮转与循环覆盖
Go 标准库没有内置环形日志(即固定大小、写满后从头覆盖)支持,但 lumberjack.Logger 是最常用、最稳妥的替代方案——它不真正“覆盖”,而是按大小切分 + 保留有限个旧文件,效果等价于环形缓冲,且避免了手动 seek 和 truncate 带来的竞态与损坏风险。
直接操作文件指针做覆盖写入,在多 goroutine 写日志、程序崩溃、或日志内容长度不固定时极易出错。别走这条路。
实操建议:
- 安装:
go get gopkg.in/natefinch/lumberjack.v2 - 初始化时设置
MaxSize(单位 MB)、MaxBackups(保留几个旧日志)、MaxAge(过期天数,可设为 0 禁用) - 把
lumberjack.Logger作为io.Writer传给log.SetOutput或zap.New(…).WithOptions(zap.AddCaller(), zap.WriteTo(…)) - 注意:它默认以追加模式打开文件,不会清空已有内容;首次运行若文件已存在且超限,会在下一次写入时触发轮转
为什么不用 os.Truncate + file.Seek(0, 0) 手动覆盖
看起来简单:日志写满后 truncate 文件、seek 到开头重写。但实际会踩多个坑:
- 并发写入时,一个 goroutine truncate 后,另一个可能正处在 write 中间,导致文件内容错乱或 syscall.EBADF
- 日志行长度不可控(比如带 traceID 的 JSON 日志),无法精确对齐“环形边界”,覆盖后大概率出现半行日志或 JSON 解析失败
- 如果进程意外退出,文件可能处于 truncate 完但未写入新内容的状态,丢失全部日志
-
lumberjack虽然不覆盖,但通过限制MaxBackups=1+MaxSize=10,实际磁盘占用就是稳定的 ~10MB,和环形文件无异
lumberjack 的关键参数怎么设才贴近“环形”语义
目标是让日志总空间可控、旧内容自动淘汰。重点调这三个字段:
-
MaxSize: 50→ 单个日志最大 50MB(根据磁盘和日志频率调整,小服务设 10–20 更安全) -
MaxBackups: 1→ 只保留当前文件 + 最近一个备份,即最多两个文件;设为 0 会禁用备份,但轮转失败时无兜底 -
LocalTime: true→ 让归档文件名含本地时间,方便调试;不影响循环逻辑 - 避免设
Compress: true,压缩会增加 I/O 开销,且解压后无法直接tail -f查看
示例片段:
logger := &lumberjack.Logger{
Filename: "/var/log/myapp/app.log",
MaxSize: 20, // MB
MaxBackups: 1,
MaxAge: 0,
LocalTime: true,
}
log.SetOutput(logger)
如果真要字节级环形覆盖(极少数场景)
仅当满足:单 writer、纯文本、每条日志定长、可接受丢弃部分尾部数据——才考虑自己实现。用 os.OpenFile 以 os.O_RDWR | os.O_CREATE 打开,配合 file.Seek 和 file.WriteAt。
- 必须加
sync.Mutex保护所有文件操作,否则 race detector 必报错 - 不能用
log.Logger,得自己封装写逻辑,因为标准log不支持 offset 写入 - 每次写前需检查剩余空间,不足时
file.Truncate(0)并重置 offset,但此时正在读该文件的tail进程会收到SIGHUP或中断 - 生产环境几乎没人这么干;
lumberjack在绝大多数 case 下更健壮、更易维护
真正容易被忽略的是:日志路径的目录权限和磁盘配额。即使代码逻辑完美,/var/log/myapp/ 目录不可写,或磁盘只剩 1KB,lumberjack 也会静默失败——记得在启动时主动 os.Stat 检查目录可写性,并记录 error 日志到 stderr。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











