go标准库log不支持自动滚动,需用lumberjack等第三方库或手动实现;lumberjack通过maxsize、maxbackups、maxage等参数控制滚动与归档,但清理逻辑不宜每次写日志都执行。

为什么 log.SetOutput 直接配 os.File 无法自动滚动
Go 标准库的 log 包本身不支持文件滚动、按大小/时间切分或自动备份。它只是把日志写进一个 io.Writer,哪怕你传入的是一个打开的 *os.File,它也不会在文件变大时关掉重开——除非你手动干预。常见错误是:启动时打开一次文件,运行几天后发现日志塞满磁盘,而旧日志早已被覆盖或根本没归档。
解决思路只有两个:自己轮询判断 + 切换文件,或者用成熟第三方库接管写入逻辑。
用 lumberjack.Logger 实现按大小滚动与保留策略
最常用且轻量的选择是 github.com/natefinch/lumberjack。它不是日志框架,而是一个可嵌入的 io.WriteCloser,专为替换日志输出目标设计。
-
MaxSize:单个日志文件最大 MB 数(默认 100),超过即切割 -
MaxBackups:最多保留几个旧日志(比如设为 7,就只留最近 7 个.log.1~.log.7) -
MaxAge:旧日志最多保留多少天(按文件修改时间判断,非写入时间) -
LocalTime:是否用本地时区命名归档文件(默认 false,用 UTC) -
Compress:是否对归档文件启用 gzip 压缩(需 Go 1.16+)
示例用法:
立即学习“go语言免费学习笔记(深入)”;
import (
"log"
"os"
"github.com/natefinch/lumberjack"
)
func main() {
logger := log.New(&lumberjack.Logger{
Filename: "app.log",
MaxSize: 10, // MB
MaxBackups: 5,
MaxAge: 28, // days
Compress: true,
}, "", log.LstdFlags)
logger.Println("this goes to rotated file")
}
手动实现按日期归档时要注意文件名冲突和并发安全
如果不想引入依赖,想按日期(如 app-2024-06-15.log)每天新建文件,必须自己控制文件切换时机。关键点不是“怎么格式化时间”,而是:切换瞬间不能丢日志,也不能多个 goroutine 同时写同一个文件句柄。
- 用
sync.Mutex保护文件句柄切换过程 - 每次写日志前检查当前日期是否变化,若变化则关闭旧文件、打开新文件
- 避免在
log.SetOutput中直接传*os.File;应封装成自定义io.Writer,实现Write([]byte) (int, error)方法,在其中做日期判断和切换 - 注意
os.OpenFile的 flag:务必用os.O_APPEND | os.O_CREATE | os.O_WRONLY,否则可能覆盖已有内容
典型坑:凌晨 00:00 多个请求同时触发切换,导致两个 goroutine 都创建了同名文件,其中一个覆盖另一个——结果某天日志完全丢失。
归档压缩与清理逻辑别放在主日志路径里执行
像 MaxAge 或 MaxBackups 这类清理动作,lumberjack 是在每次写日志前检查并执行的。这意味着如果日志写得非常频繁(比如每毫秒一条),它就会每秒都去扫描目录、stat 文件、删除旧文件——带来不必要的 I/O 和锁竞争。
更稳妥的做法是:把归档策略交给外部定时任务(如 cron + shell 脚本),或在程序中用独立 goroutine 每小时检查一次,而不是每次写都查。尤其当 MaxAge 设为 30 天、目录下有上百个归档时,每次写前遍历会明显拖慢主流程。
真正容易被忽略的是:归档文件名中的时间戳,是文件的 ModTime(),不是日志内容里的打印时间。如果你用 rsync 同步日志目录,或人为 touch 过文件,lumberjack 可能误判“这个文件还很新”,导致该删的没删。










