直接用 os.file.write 会丢日志,因其仅写入内核缓冲区而不刷盘;安全写入需行缓冲加 file.sync(),开发期每次写后 sync,生产环境用 bufio.writer 批量 flush;log.setoutput 可重定向日志到文件,须以追加模式打开;需轮转防文件过大,可用 lumberjack;异步写日志易致乱序或漏写,应避免自行 goroutine 化。

为什么直接用 os.File.Write 会丢日志
很多初学者直接打开文件后循环调用 file.Write,发现程序退出时最后一段日志没落盘。这是因为 Go 的 Write 默认只写入内核缓冲区,不强制刷盘;进程崩溃或异常退出时缓冲区内容就丢了。
真正安全的持续写入必须满足两个条件:行缓冲(每行及时写出)+ 强制刷盘(file.Sync())。但频繁 Sync() 会影响性能,所以得权衡。
- 开发/调试阶段:每次写完立刻
file.Sync(),确保日志不丢失 - 生产环境:用
bufio.NewWriter做小批量缓冲(比如 4KB),再定期Flush(),避免磁盘 I/O 过载 - 永远不要依赖
defer file.Close()来保证最后刷盘——如果程序 panic,defer可能来不及执行
用 log.SetOutput 接管标准日志输出到文件
Go 标准库的 log 包本身不支持自动轮转或异步写入,但它允许你把输出重定向到任意 io.Writer。只要把打开的 *os.File 传给 log.SetOutput,就能让所有 log.Println 写进文件。
注意点:
- 文件必须以
os.O_CREATE | os.O_WRONLY | os.O_APPEND模式打开,否则日志会覆盖而非追加 - 如果多个 goroutine 同时调用
log.Println,标准log包内部已加锁,无需额外同步 - 别用
log.SetFlags(0)关掉时间戳——没有时间戳的日志在排查问题时基本没法用
示例片段:
file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
log.SetOutput(file)
log.Println("server started") // 这行会写入 app.log
如何避免日志文件无限增长
持续写入不处理轮转,几个月后可能生成几十 GB 的单个日志文件,既难查看又难备份。Go 标准库不提供轮转功能,必须自己实现或引入轻量库。
简单可靠的自实现思路:
- 每天零点检查当前日志文件名是否含当天日期(如
app-2024-06-15.log),不符则关闭旧文件、新建带日期的文件 - 用
os.Stat查文件大小,超过 100MB 就切新文件(注意:不能只靠大小,否则一天内超大请求会卡死轮转) - 轮转时用
os.Rename移动旧文件,避免拷贝开销;新文件仍用O_APPEND打开 - 切文件前务必先
file.Sync()再file.Close(),否则最后一段日志可能丢失
更省事的做法是用 lumberjack 库(仅一个文件,无依赖):lumberjack.Logger 实现了大小+时间双策略轮转,直接传给 log.SetOutput 即可。
goroutine 安全写日志的常见误区
有人为“提高性能”把日志写入扔进 goroutine 异步执行,结果出现乱序、panic 或漏写。根本原因是:os.File.Write 本身是并发安全的,但异步化引入了新的竞态点。
- 如果在 goroutine 里用局部变量接收日志内容,而主 goroutine 已经释放该内存(比如字符串底层数组被复用),异步写入可能读到脏数据
- 多个 goroutine 共享一个未加锁的
bufio.Writer,WriteString调用会互相覆盖缓冲区 - 用 channel 做日志队列看似优雅,但若消费者阻塞(比如磁盘满),生产者会卡住甚至拖垮整个服务
真正稳妥的做法是:保持同步写入,但把耗时操作(如格式化时间、获取调用栈)提前做完,再原子地写入文件。或者用成熟的结构化日志库(如 zerolog)自带异步模式,它内部已处理好内存拷贝和背压。
最常被忽略的一点:日志文件的目录权限和磁盘空间监控。程序启动时检查 os.MkdirAll 是否成功,运行中定时用 syscall.Statfs 检查剩余空间,低于 5% 就发告警——否则某天你会发现所有日志都静默失败了,连错误提示都没留下。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











