log.SetOutput不能直接实现按日期切分,因其仅设置固定io.Writer,无法自动感知日期变更并切换文件;手动替换会引发竞态和资源泄漏,需用lumberjack等库或自封装线程安全的日期感知写入器。

为什么 log.SetOutput 不能直接解决按日期切分日志的问题
Go 标准库的 log 包本身不支持自动按日期轮转文件。调用 log.SetOutput 只能设置一个固定的 io.Writer,比如一个打开的 *os.File;但一旦文件句柄创建好,它不会在日期变更时自动关闭旧文件、打开新文件。强行在程序中手动判断日期并替换 log.SetOutput,会引发竞态:多 goroutine 写日志时可能写入错误文件,或旧文件句柄未及时关闭导致资源泄漏。
用 lumberjack.Logger 实现日期感知的日志切分(推荐)
lumberjack 是最常用的 Go 日志轮转库,虽然它原生按大小(MaxSize)和保留天数(MaxAge)轮转,但可以通过组合策略实现“每天一个文件”效果:
- 把
MaxSize设得足够大(如1024 * 1024 * 1024),确保单日日志不会触发大小切分 - 启用
LocalTime: true,让归档文件名使用本地时间而非 UTC - 设置
MaxAge: 7(保留最近 7 天文件),配合每日归档逻辑 - 关键点:归档动作由写入触发,不是定时器驱动 —— 所以需确保每天至少有一次写入,否则当天文件不会生成(可加启动时写一条空日志兜底)
示例初始化:
import (
"log"
"os"
"time"
"gopkg.in/natefinch/lumberjack.v2"
)
<p>func newRotatingLogger() *log.Logger {
writer := &lumberjack.Logger{
Filename: "./logs/app-" + time.Now().Format("2006-01-02") + ".log",
MaxSize: 1024, // MB,设大些避免误切
MaxBackups: 7,
MaxAge: 7,
LocalTime: true,
Compress: false,
}
return log.New(writer, "", log.LstdFlags)
}自己封装一个线程安全的日期感知 io.WriteCloser
如果不想引入第三方依赖,或需要更精确控制(比如严格按 00:00 切分,哪怕当天无日志也生成空文件),就得自己维护当前日期对应的文件句柄,并在写入前检查是否跨天:
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.RWMutex保护文件句柄和当前日期字段 - 每次写入前,用
time.Now().Format("2006-01-02")比较当前日期与缓存日期,不一致则先Close()旧文件,再os.OpenFile()新文件 - 注意:不要在
Write()方法里做耗时操作(如磁盘 I/O),否则会拖慢所有日志写入;可考虑预热下一天文件句柄,或接受首次跨天写入稍慢 - 务必实现
Close()方法,否则最后一天文件可能没被正确关闭
核心判断逻辑类似:
func (w *dateWriter) Write(p []byte) (n int, err error) {
today := time.Now().Format("2006-01-02")
w.mu.RLock()
sameDay := w.date == today
w.mu.RUnlock()
<pre class="brush:php;toolbar:false;">if !sameDay {
w.mu.Lock()
if w.date != today { // double-check
if w.file != nil {
w.file.Close()
}
w.file, _ = os.OpenFile("./logs/app-" + today + ".log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
w.date = today
}
w.mu.Unlock()
}
return w.file.Write(p)}
注意 Windows 下文件重命名冲突和权限问题
在 Windows 上,如果日志文件正被其他进程(如文本编辑器、IDE)打开,lumberjack 的归档操作(os.Rename)会失败,报错 The process cannot access the file because it is being used by another process。这不是 Go 代码 bug,而是系统限制:
- 避免用记事本等非独占方式打开日志文件;推荐用 VS Code、Notepad++ 等支持“重新加载外部更改”的编辑器
- 若必须支持热读,可在
lumberjack初始化时设Compress: true,归档后压缩成 zip,降低被锁风险 - Windows 服务场景下,确保运行账户对日志目录有写权限,且路径不含中文或空格(部分旧版
lumberjack对路径编码处理不完善)
跨天逻辑本身不复杂,真正容易出问题的是并发写入时的状态同步、异常恢复(如崩溃后文件未关闭)、以及不同操作系统对文件句柄的语义差异 —— 这些细节比“怎么格式化日期”重要得多。


















