Go log包默认输出到stderr,写文件需用SetOutput重定向至os.File;推荐OpenFile标志os.O_APPEND|os.O_CREATE|os.O_WRONLY;须检查error、close文件;多goroutine下应共用单个Logger或加锁writer;shell重定向不可靠;简单按天轮转需加锁封装,复杂需求用lumberjack。

Go 标准库的 log 包默认输出到 os.Stderr,不直接写文件;要写入日志文件,必须显式设置输出目标,且需注意文件打开模式、并发安全和资源释放。
用 log.SetOutput() 重定向到文件
最直接的方式是把 *os.File 传给 log.SetOutput()。但要注意:os.OpenFile() 的标志位决定是否追加、是否创建、是否截断。
-
os.O_APPEND | os.O_CREATE | os.O_WRONLY:推荐组合,追加写、自动建文件、只写不读 - 避免用
os.O_TRUNC(会清空已有日志) - 务必检查
os.OpenFile()返回的 error,路径不存在或权限不足时会失败 - 程序退出前应调用
file.Close(),否则可能丢失最后几条日志(缓冲未刷出)
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
log.SetOutput(file)
log.Println("服务启动") // 写入 app.log
多 goroutine 写日志时为什么会出现乱序或丢日志
log.Logger 本身是并发安全的,但底层 io.Writer(比如 *os.File)的写入操作在高并发下仍可能因系统调用调度或缓冲区竞争导致行粘连(如两行日志挤在同一行),尤其当每条日志未以换行结尾或写入未原子化时。
- 标准
log默认每条日志末尾加\n,所以只要不手动改log.SetFlags(0)或用log.Print()混用,一般不会粘连 - 真正风险点在于:多个
log.Logger实例共用同一个*os.File,且各自调用Write()—— 这绕过了log的锁机制 - 解决方案:始终只用一个
log.Logger实例,或使用带锁封装的 writer(如io.MultiWriter+ 自定义同步 writer)
为什么不能直接用 log.Printf() 配合 os.Stdout 重定向来写文件
在 shell 启动时用 ./app > app.log 看似可行,但存在严重缺陷:
- 所有
log.Println()输出会被重定向,但log.Fatalln()和log.Panicln()仍会写到 stderr(除非也重定向2>&1) - 无法控制日志格式(如无时间戳、无级别标识)
- 进程崩溃或被 kill -9 时,stdout 缓冲区内容大概率丢失
- 无法按大小轮转、无法压缩归档——纯靠外部工具(如 logrotate)管理,耦合度高
简单轮转需求:自己封装一个按天切分的 io.Writer
如果不需要完整日志库(如 zap 或 lumberjack),可轻量封装:每次写前检查当前日期是否变化,变化则关闭旧文件、打开新文件。
- 关键点:写操作必须加互斥锁(
sync.Mutex),否则多 goroutine 下可能同时触发切换,导致文件句柄泄漏或写入错位 - 不要在
Write()方法里做耗时操作(如 stat、rename),否则拖慢所有日志调用 - 建议缓存今日文件名,仅在
Write()开头比对time.Now().Format("2006-01-02"),避免高频调用time.Now()
真正复杂的轮转(按大小、保留份数、压缩、异步刷盘)还是交给 github.com/natefinch/lumberjack 更稳妥——它内部已处理了竞态、原子重命名和 close race。









