直接用 file.Write 会写乱文件,因多个 goroutine 共享 *os.File 导致偏移量和缓冲区竞争;唯一内核级安全方案是 os.O_APPEND,需配合 os.O_WRONGLY 和 os.O_CREATE 使用,且不可 Seek 或套 bufio.Writer;推荐单 writer goroutine + channel 模式,配合 file.Sync() 确保落盘。

直接用 file.Write 会写乱文件,原因不是并发本身,而是偏移量和缓冲区竞争
多个 goroutine 共享同一个 *os.File 并调用 Write 或 WriteString,最常见现象是:两行日志拼在一起(如 "ReqID:123" 和 "status:200" 变成 "ReqID:st200"),或某次写入被截断、覆盖。这不是 Go 运行时 bug,而是底层行为决定的:os.File 不带锁,Write 可能被拆成多次系统调用,且所有 goroutine 共享同一文件偏移量。即使加 sync.Mutex,也只是把并发变成串行,没解决根本问题。
用 os.O_APPEND 是唯一由内核保障安全的追加方案,但仅限追加
Linux 下,以 os.O_APPEND | os.O_WRONLY 打开文件后,每次 write(2) 系统调用都会自动定位到文件末尾再写入,这个操作是原子的。Go 层只要确保每次 Write 写的是完整一行(不跨调用拆分),就能避免交错。
- 必须用
os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644),不能只用os.O_WRONLY - 不能用于覆盖写、随机写,也不能靠
Seek改变位置——一旦Seek,就脱离了O_APPEND保护 -
bufio.Writer不能直接套在O_APPEND文件上使用,因为它的缓冲写入会绕过原子追加语义;如需缓冲,得自己控制 flush 时机并确保每次Write是完整逻辑单元
推荐方案:单 writer goroutine + channel,适合日志、审计等高频小数据场景
这是最通用、可控性最强的做法。核心是把写入逻辑收归一个 goroutine,其他协程只负责发消息,天然规避竞态。
- 定义带缓冲的 channel:
logCh := make(chan string, 1024),容量按峰值 QPS × 平均处理延迟预估 - 先启动消费者:
go func() { defer file.Close(); for msg := range logCh { _, _ = file.WriteString(msg); file.Sync() } }()(注意file.Sync()保证落盘,否则可能丢日志) - 生产者直接
logCh ,无需关心锁或偏移量 - 务必在所有生产者退出后调用
close(logCh),否则消费者 goroutine 永远阻塞在range - 不要在消费者函数里用
defer file.Close()后再for range——defer要等函数返回才执行,而range不退出,文件句柄就一直开着
每个 goroutine 写独立文件再合并,适合导出、分片等批处理场景
当写入内容可分割(如按用户 ID、时间戳、批次号),且最终需要合并为一个结果时,这种方案更稳妥、易调试、支持失败重试。
- 用
uuid.NewString()或time.Now().UnixNano()生成唯一文件名,避免覆盖 - 每个 goroutine 自己调用
os.Create()或os.OpenFile(..., os.O_CREATE|os.O_WRONLY),互不干扰 - 主 goroutine 用
io.Copy顺序合并,比逐行读取拼接更省内存 - 合并完成后,可选是否清理临时文件(
os.Remove)
真正容易被忽略的是 channel 的生命周期管理:消费者必须早于生产者启动,通道必须显式关闭,且写入操作要检查错误返回值——静默丢弃数据比写乱更难排查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











