标准 log 包不支持轮转因其仅提供基础输出能力,不感知文件生命周期,需手动实现或借助 lumberjack 等第三方包;lumberjack 是目前最稳定方案,支持按大小、天数、备份数轮转并可压缩。

为什么标准 log 包不支持轮转
Go 标准库的 log 包只提供基础输出能力,log.SetOutput 接收一个 io.Writer,但本身不感知文件生命周期。它不会自动检测文件大小、日期变化或重命名旧日志——这些都得你手动实现或交给第三方包处理。
直接用 os.OpenFile 配合 log.SetOutput 写入单个文件,一旦进程长期运行,日志会无限增长,磁盘迟早被撑爆。
用 lumberjack 实现开箱即用的轮转
目前最稳定、被广泛采用的方案是 github.com/natefinch/lumberjack。它是一个轻量级 io.WriteCloser 实现,可无缝注入到标准 log 中,支持按大小、保留天数、最大备份数控制轮转行为。
安装:
go get github.com/natefinch/lumberjack
立即学习“go语言免费学习笔记(深入)”;
典型配置示例:
package main
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 will go to rotated file")
}
-
MaxSize是触发轮转的单个日志文件大小(单位 MB),不是字节;设为0表示不按大小轮转 -
MaxAge和MaxBackups是独立策略:前者删过期文件(按最后修改时间),后者删最老的备份,二者可能同时生效 -
Compress: true仅在轮转后对.gz压缩,不影响当前写入文件;压缩失败不会中断日志 - 注意:该包不支持 Windows 下的文件独占锁,多进程写同一日志文件时仍可能冲突
手动轮转需自己处理的几个关键点
如果出于极简依赖或特殊需求必须手写轮转逻辑,核心难点不在“重命名文件”,而在于原子性、并发安全和写入不丢日志。
- 不能先
Close()再os.Rename():中间窗口期新日志会丢失 - 必须用
os.OpenFile(..., os.O_APPEND|os.O_CREATE|os.O_WRONLY)打开,否则重定向后写入位置可能错乱 - 轮转判断(如检查文件大小)应放在每次写入前,或用定时器异步检查,但要注意避免重复轮转
- 多 goroutine 写日志时,必须加锁保护轮转动作,否则
rename和write可能交错导致 panic 或数据错位
换句话说:手写轮转不是“写个 rename 就完事”,而是要模拟一个带状态机的 writer,比引入 lumberjack 多出至少 200 行易出错代码。
日志路径和权限问题常被忽略
lumberjack 或自定义轮转器都依赖底层 os 操作,以下情况会导致静默失败或 panic:
- 指定的
Filename路径上级目录不存在(lumberjack不自动创建,需提前os.MkdirAll) - 进程无权在目标目录创建/重命名文件(尤其容器中挂载的只读卷或非 root 用户运行)
- 日志文件被外部命令(如
tail -f或 logrotate)占用,Linux 下虽通常不影响写入,但lumberjack的MaxAge清理可能失败
建议启动时加一段校验:
if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil {
log.Fatal("failed to create log dir:", err)
}
轮转逻辑越靠底层,对文件系统行为的假设就越具体。生产环境别绕过成熟封装去拼凑路径和权限处理。









