Go中并发读写同一*os.File不安全,因其共享文件偏移量导致数据错乱、覆盖或panic;必须用sync.Mutex或sync.RWMutex同步,且锁需包裹全部文件操作,避免锁内耗时操作和漏解锁。

Go 中对同一文件做并发读写时,os.File 本身不是线程安全的——多个 goroutine 直接调用 Write 或 ReadAt 可能导致数据错乱、偏移错位甚至 panic。必须手动加锁,但锁的粒度和方式选错,反而会引入死锁或性能瓶颈。
为什么不能直接用 os.File 的方法做并发读写
os.File 底层封装了系统文件描述符,其 Read/Write 方法会修改内部的读写偏移(offset),这个偏移是共享状态。多个 goroutine 并发调用时,偏移更新不原子,结果不可预测。例如:
- 两个 goroutine 同时
Write,可能互相覆盖写入位置 - 一个 goroutine
Seek后Read,另一个同时Write,Seek位置被冲掉 -
ReadAt和WriteAt虽然带 offset 参数,但它们仍依赖底层 fd 的一致性,Go 标准库未保证并发调用安全
sync.Mutex 锁住整个文件操作是最简单但最粗暴的方式
适用于写少、逻辑简单、吞吐量不敏感的场景。关键点是:锁必须包裹所有涉及该文件的操作,包括 Open、Read、Write、Close 等。
- 不要只锁
Write却放任Read并发——读写之间也需互斥 - 避免在锁内做耗时操作(如网络请求、复杂计算),否则严重拖慢其他 goroutine
-
defer mu.Unlock()必须紧贴mu.Lock(),且确保所有 return 路径都执行到 - 不要复制已使用的
sync.Mutex值,否则 panic;应始终作为 struct 字段或包级变量使用
示例:mu 是包级 sync.Mutex:
立即学习“go语言免费学习笔记(深入)”;
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
func writeFile(filename string, data []byte) error {
mu.Lock()
defer mu.Unlock()
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = f.Write(data)
return err
}
读多写少时用 sync.RWMutex 提升并发读性能
当多个 goroutine 频繁读、极少写(比如配置文件热加载),用 RWMutex 可让读操作并行,写操作独占。
-
RLock/RUnlock用于读操作;Lock/Unlock用于写操作 - 写操作会阻塞所有新读请求,且等待已有读完成;读操作不会阻塞其他读
- 注意:如果写操作频繁,RWMutex 可能比 Mutex 更慢(因额外状态管理)
-
RUnlock对未RLock的实例不 panic,但Unlock对未Lock的实例会 panic
典型误用:RLock 后忘记 RUnlock,导致后续所有写永久阻塞。
更健壮的做法:把文件操作封装成带锁的结构体
避免全局锁污染,也防止锁粒度失控。重点在于锁的生命周期与文件句柄绑定,而非与业务逻辑耦合。
- 将
*os.File和sync.RWMutex一起嵌入 struct,对外只暴露带锁的方法 - 写操作用
Lock,读操作用RLock,并在方法内完成Seek、Read、Write全流程 - 不要暴露裸
*os.File,否则外部绕过锁直接操作会破坏一致性 - 考虑是否需要
sync.Once控制文件首次打开,避免重复Open
容易被忽略的一点:即使用了锁,os.File 在多进程环境下仍不安全(如两个 Go 进程同时写同一文件),此时需用文件锁(syscall.Flock)或外部协调机制。

















