并发写文件会出问题,因多个goroutine共享*os.File时file.Write不保证原子性,Go层可能将一次写拆为多次系统调用,导致内容交错;log.SetOutput()修改全局logger造成逻辑覆盖;仅os.O_APPEND由内核保障追加原子性,但通用方案是单writer goroutine+channel串行化。

直接用 log.Println 或 file.Write 并发写同一个文件,必然乱序甚至内容损坏;唯一内核级安全的追加方案是 os.O_APPEND,但通用且可控的做法是单 writer goroutine + channel 串行化。
为什么并发写文件会出问题
多个 goroutine 共享一个 *os.File 句柄时,file.Write 不保证原子性:Go 层可能把一次 []byte 拆成多次 write() 系统调用;若两个 goroutine 同时写,就可能出现 “ReqID:123” 和 “status:200” 交错成 “ReqID:st200”。log.SetOutput() 更危险——它改的是全局 log.Logger 实例,后调用者覆盖前者的输出目标,不是竞态而是逻辑覆盖。
- Linux 下只有
os.O_APPEND能由内核保障追加写入的原子性(每次write()自动 seek 到末尾再写) -
sync.Mutex+file.Seek()组合必须全程加锁,否则Seek和Write之间仍会被抢占 - 不要试图用
bufio.Writer单独包裹每个 goroutine 的*os.File——缓冲区不共享,落盘时机不可控,依然会乱
用单 writer goroutine + channel 串行写入
这是最灵活、可扩展、易调试的方案,适合日志、审计、导出等高频小数据写入场景。
- 初始化时用
os.OpenFile(name, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644),配合sync.Once避免重复打开 - 定义带缓冲的 channel:
logCh := make(chan string, 1024),容量按峰值 QPS × 平均延迟预估(例如 1000 QPS × 10ms = 10 条) - 启动唯一后台 goroutine:
go func() { defer file.Close(); for msg := range logCh { file.WriteString(msg); file.Sync() } }() - 业务 goroutine 只做
logCh ,绝不碰 <code>*os.File -
file.Sync()保证落盘,但拖慢吞吐;若允许少量丢失(如非关键日志),可去掉,改用定时 flush 或 buffer 达阈值后刷盘
WaitGroup 在文件写入流程中的误用陷阱
WaitGroup 本身不参与 I/O 同步,但它常被错误地用于“等所有写请求发完”,结果反而掩盖了真正的问题。
立即学习“go语言免费学习笔记(深入)”;
- 在 handler 中声明
var wg sync.WaitGroup后传值给子函数,会导致wg.Add()和wg.Wait()操作不同实例 →Wait()永远阻塞 - 忘记在 goroutine 内部用
defer wg.Done(),或提前 return 漏掉,导致计数器不归零 → 请求卡死超时 - 闭包捕获循环变量:
for _, url := range urls { go func() { ... }() }→ 所有 goroutine 共享最后一个url值,部分写入被跳过 - 正确做法:每请求独立声明
wg,wg.Add(1)必须在go语句前,goroutine 内第一行就是defer wg.Done(),传参显式写go f(url)
真正难的不是选方案,而是意识到:文件写入的“同步”本质是**顺序化**,不是“锁住”或“等完成”。os.O_APPEND 是内核给的捷径,但一旦需要格式控制、错误重试、限流或混合读写,就必须退回到 channel + 单 writer 这个明确责任边界的模型。任何试图让多个 goroutine 直接操作同一文件句柄的尝试,都在和调度器与系统调用的不确定性博弈——赢的概率随并发量指数下降。


















