Go标准库log不支持滚动因仅提供基础输出且不干预写入逻辑;需用lumberjack等第三方工具或手动实现,但后者不适用于高并发长期服务。

为什么 log 标准库本身不支持滚动
Go 的 log 包只提供基础输出能力,SetOutput 接收一个 io.Writer,但不会干预写入逻辑——它不管文件是否满、要不要切分、旧日志要不要删。想滚动,得自己构造一个带滚动行为的 io.Writer,或者换更成熟的日志库。
用 lumberjack 实现开箱即用的滚动
最常用且稳定的选择是 github.com/natefinch/lumberjack。它不是日志框架,而是一个「可滚动的 io.WriteCloser」,能无缝对接 log.SetOutput 或 zap/logrus 等结构化日志库。
常见配置项含义和易错点:
-
Filename:必须是绝对路径或确保进程有写权限的相对路径;若目录不存在,lumberjack不会自动创建,会静默失败(日志全丢) -
MaxSize(单位 MB):触发滚动的单文件大小阈值,设为10表示 10MB 后切新文件;注意它检查的是「写入前」文件大小,不是实时统计 -
MaxBackups:保留的旧日志文件数,设为3则最多存app.log、app.log.1、app.log.2、app.log.3,再滚动时删掉最老的.3 -
MaxAge(单位天):按修改时间清理,和MaxBackups是「或」关系——任一条件满足就清理,不是同时满足 -
Compress:设为true会用gzip压缩旧文件,但压缩过程阻塞写入,高并发写日志时可能卡住
最小可用示例:
import (
"log"
"os"
"github.com/natefinch/lumberjack"
)
func main() {
logger := log.New(&lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 10,
MaxBackups: 3,
MaxAge: 28,
Compress: false,
}, "", log.LstdFlags)
logger.Println("hello world")
}
手动实现简单轮转(仅限调试/低频场景)
如果不想加依赖,且日志量极小(如嵌入式设备、脚本工具),可以自己封装一个带时间戳轮转的 io.Writer。但要注意:它不处理并发写、不校验磁盘空间、不保证原子切换。
关键逻辑点:
- 每次写前检查当前文件名是否匹配「今天」,不匹配就关闭旧文件、新建带日期后缀的文件(如
app-2024-06-15.log) - 必须用
os.O_CREATE | os.O_APPEND | os.O_WRONLY打开文件,否则会清空 - 不能在
Write方法里做耗时操作(比如遍历目录删旧文件),否则拖慢所有日志调用 - 没有锁保护时,多 goroutine 并发写会导致文件句柄错乱或 panic
这种写法适合一次性脚本,不适合长期运行的服务。
滚动时容易被忽略的权限与路径问题
线上部署最常踩的坑不是代码逻辑,而是环境:
- 二进制运行用户对
Filename所在目录没有w权限,lumberjack创建文件失败,但不会报错,日志直接消失 - 使用相对路径(如
"logs/app.log")时,实际路径取决于进程启动时的pwd,Docker 或 systemd 启动时 pwd 往往是/或/root,导致日志写到意外位置 - 日志目录被 mount 为只读(如某些容器安全策略),同样静默失败
-
MaxBackups和MaxAge清理时机是「每次打开文件时」,不是后台定时任务——如果服务长期不重启、也不触发滚动,旧日志永远不会被删
上线前务必用真实用户身份手动执行一次写入,检查目标路径是否存在、是否可写、是否有残留旧文件未被清理。


















