多个goroutine并发写同一文件会导致内容覆盖、截断或交错,因O_TRUNC重置文件且write()非原子;需复用*os.File并用sync.Mutex保护,配合bufio.Writer缓冲及显式flush,同时检查Write返回值。

多个 goroutine 同时写同一个文件会出什么问题
直接并发调用 os.WriteFile 或反复 os.OpenFile(..., os.O_WRONLY|os.O_CREATE|os.O_TRUNC) 写同一路径,会导致文件内容被随机覆盖或截断——因为每次打开都带 O_TRUNC,且写入无顺序保证。更隐蔽的是用 os.O_APPEND 看似安全,但在 Linux 上若文件描述符未设 O_APPEND(比如通过 dup 复制),或在 NFS 等特殊文件系统上,仍可能产生交错写入。
- 现象:日志文件出现乱码、半截 JSON、缺失字段,甚至空文件
- 根本原因:系统调用
write()本身不是原子的,尤其当写入超过 PIPE_BUF(通常 4KB)时,内核可能拆成多次 syscall - 不要依赖
os.O_APPEND做“并发安全”的错觉——它只保证每次write()追加到当前 EOF,不保证多 goroutine 调用间的执行顺序
用 sync.Mutex 保护文件句柄是否足够
对单个 *os.File 实例加锁能避免竞态,但要注意锁的粒度和生命周期。如果每次写都 os.OpenFile → 写 → Close,锁没意义;必须复用文件句柄,并确保所有写操作都走同一把锁。
- 正确做法:在初始化时打开一次文件(如用
os.O_WRONLY | os.O_CREATE | os.O_APPEND),全局持有一个*os.File和一个sync.Mutex - 错误做法:锁包裹
os.WriteFile——它内部会打开/关闭文件,锁完全无效 - 注意
defer f.Close()不能放在 goroutine 内部,否则可能提前关闭;应在程序退出前统一关闭
var (
logFile *os.File
logMu sync.Mutex
)
func init() {
f, err := os.OpenFile("app.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
logFile = f
}
func writeLog(msg string) {
logMu.Lock()
defer logMu.Unlock()
logFile.Write([]byte(msg + "\n"))
}
高并发下推荐用 bufio.Writer 配合定期 flush
纯 Write 系统调用开销大,尤其小数据高频写。用 bufio.NewWriterSize(f, 4096) 缓冲后批量落盘,能显著降低 syscall 次数和锁争用时间。
- 缓冲区大小建议设为 4KB 或 8KB;太小失去缓冲意义,太大增加延迟和内存占用
- 必须显式调用
w.Flush(),否则内容可能滞留在内存中不写入磁盘 - 可在定时器(
time.Ticker)或写入量达到阈值时触发 flush,避免日志丢失(如进程崩溃) - 注意:
bufio.Writer本身不是并发安全的,仍需外部锁保护
真正需要隔离写入时,考虑按 goroutine 分文件或用 channel 聚合
当写入逻辑差异大(如不同模块日志格式不同)、或单文件 I/O 成为瓶颈时,硬塞进一个文件+一把锁反而降低吞吐。此时应让并发写入“解耦”。
- 方案一:每个 goroutine 写独立临时文件(如
log_worker_12345.log),由后台 goroutine 定期合并 - 方案二:所有写请求发到一个带缓冲的
chan string,单个消费者 goroutine 串行处理并写入——本质是把并发转为生产者-消费者模型 - 方案三:用第三方库如
lumberjack(支持轮转、压缩、并发安全),它内部已封装了文件锁和缓冲策略
最易被忽略的一点:无论用哪种方式,都要检查 Write 的返回值。磁盘满、权限不足、NFS 挂载失效等错误不会 panic,但会静默失败——尤其在大量 goroutine 中,一个 err != nil 被忽略,可能让整条日志链路中断而不自知。










