Go标准库log包不支持自动清理,因其仅负责写入而不管理文件生命周期;必须结合lumberjack轮转与time.Ticker定时扫描删除过期日志,或改用zerolog+file-rotatelogs实现原生自动清理。

Go 程序无法靠 log 包自身完成日志定时清理,必须主动集成文件轮转与定时触发逻辑。
为什么 log 包不支持自动清理
标准库 log 包只负责写入,不感知文件生命周期。它没有内置的按时间/大小切分、过期删除、归档压缩等能力。直接用 os.OpenFile 配合 log.SetOutput 写文件,日志会无限追加,磁盘迟早被打满。
常见错误现象:log file grows endlessly、no space left on device、运维巡检时发现数 GB 的单个 app.log 文件。
- 不要试图给
log.SetOutput传一个定期重定向的*os.File—— 竞态和 close 漏洞极难控制 - 不要手写
time.Ticker+os.RemoveAll清理旧文件 —— 缺少文件名规范会导致误删或漏删 - 轮转逻辑必须和日志写入路径、命名规则、保留策略强绑定,否则清理和写入“各干各的”
推荐方案:用 lumberjack + time.Ticker 组合
lumberjack(gopkg.in/natefinch/lumberjack.v2)是事实标准的 Go 日志轮转库,它能按大小切分、保留指定数量文件、自动压缩(需额外配置),但**不提供定时清理过期文件的能力** —— 它只管“新日志该写哪”,不管“老日志该不该删”。所以需要补一层定时扫描。
立即学习“go语言免费学习笔记(深入)”;
实操建议:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用
lumberjack.Logger作为log.SetOutput的目标,设置MaxSize(如100MB)、MaxBackups(如7)、MaxAge(如30天)—— 这些参数控制轮转行为,但MaxAge在lumberjack中仅用于判断是否应触发轮转,不会自动删除超龄文件 - 单独启一个 goroutine,用
time.Ticker每小时扫描一次日志目录,按文件修改时间(os.FileInfo.ModTime())比对,删除早于now.AddDate(0,0,-30)的旧日志(注意:必须和lumberjack的命名格式一致,如app.log.2024-05-01_02或app.log.1.gz) - 扫描时务必用
filepath.Glob匹配具体模式(如"app.log.*"),避免误删其他文件;删除前加os.Stat校验是否为常规文件
示例片段(清理逻辑):
go func() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
files, _ := filepath.Glob("logs/app.log.*")
cutoff := time.Now().AddDate(0, 0, -30)
for _, f := range files {
if info, err := os.Stat(f); err == nil && info.Mode().IsRegular() && info.ModTime().Before(cutoff) {
os.Remove(f)
}
}
}
}()更省心的选择:换用 zerolog + file-rotatelogs
如果项目已用或可引入结构化日志库,zerolog 配合 github.com/lestrrat-go/file-rotatelogs 是更紧凑的组合。后者原生支持按时间轮转(WithRotationTime)和自动清理(WithMaxAge),且无须手动扫描。
关键点:
-
file-rotatelogs的WithMaxAge是真删除 —— 它在每次打开新文件时,自动清理超龄备份,无需额外 goroutine - 必须确保
RotateLogs实例被复用(如设为包变量),否则每次 new 都会丢失清理上下文 - 注意 Windows 下文件锁问题:若进程未正常退出,旧日志文件可能残留,
WithLinkName可缓解(通过软链指向当前日志)
初始化示例:
rl, _ := rotatelogs.New("logs/app.%Y%m%d.log",
rotatelogs.WithMaxAge(30*24*time.Hour),
rotatelogs.WithRotationTime(24*time.Hour),
)
logger := zerolog.New(rl).With().Timestamp().Logger()容易被忽略的边界情况
所有方案都绕不开这几个硬伤,不处理就会线上出事:
- 多实例部署时,多个进程同时扫描删除同一目录 → 必须加分布式锁(如 Redis SETNX)或改用单点日志收集服务(Filebeat + Logstash)
- 日志路径含中文或空格 →
filepath.Glob在某些系统下行为异常,统一用filepath.WalkDir+ 手动匹配更稳 -
os.Remove删除正在被写入的文件(Windows 上会失败,Linux 虽可删但 inode 仍占磁盘)→ 务必确认lumberjack或rotatelogs已彻底 close 旧文件句柄(它们默认做,但自定义 Writer 时易遗漏) - 容器环境(如 Kubernetes)中,日志目录挂载为 emptyDir 或 hostPath → 清理逻辑必须适配 volume 生命周期,避免重启后清理失效
最麻烦的从来不是“怎么删”,而是“删谁”和“谁在写”的一致性 —— 命名规则、时间解析、权限校验、并发安全,漏掉任何一环,清理就变成定时删库。

















