os.file.write 不能多 goroutine 并发写同一文件,因内核强制串行化共享 fd 的偏移量,lseek+write 易被中断导致覆盖;writeat 调用 pwrite64(2),不依赖当前偏移,可安全并发写。

os.File.WriteAt 是唯一能安全并行写入大文件的路径,但必须手动管理偏移、预分配空间、共享文件句柄,并校验返回值。
为什么不能用 os.File.Write 多 goroutine 写同一文件
Linux 内核对非 O_APPEND 文件的 write(2) 调用强制串行化:所有 goroutine 共享同一个 fd 的内核文件偏移量,lseek + write 过程中极易被其他 goroutine 打断,导致数据覆盖或错位。这不是 Go 的 bug,是系统约束。
-
WriteAt底层调用pwrite64(2),不依赖当前偏移,只认你传的off int64 - 一旦打开文件时加了
os.O_APPEND,再调WriteAt会返回ESPIPE错误 - 多个 goroutine 各自
os.OpenFile同一路径(即使都O_WRONLY),写入的是不同 fd,无法保证物理文件一致性
分块写入前必须预分配文件空间
不预分配直接 WriteAt 写末尾偏移(如 size-1),ext4/xfs 等文件系统会生成稀疏文件:中间空洞读出来全是零,且某些场景下 WriteAt 返回成功但磁盘未真正落盘。
- 推荐方式:
f.Truncate(size)或f.WriteAt([]byte{0}, size-1) - 注意
Truncate本身不是原子操作,若中途崩溃,文件可能被截短;更稳妥的是先WriteAt最后一个字节占位,再逐段填充 - 预分配大小必须等于最终文件总长,不能靠
os.Stat动态查——并发写期间长度不可信
偏移量必须由主 goroutine 静态划分,不能动态计算
任何基于 f.Seek(0, io.SeekCurrent) 或 os.Stat 获取当前偏移的做法,在并发下完全失效。其他 goroutine 可能在你 Seek 后立刻写入一段,你的计算立刻过期。
- 主 goroutine 按
chunkSize = totalSize / runtime.NumCPU()划分区间,显式传给每个子 goroutine - 每个子 goroutine 接收参数:
data []byte、offset int64、f *os.File - 务必校验
n, err := f.WriteAt(data, offset)中的n == len(data),否则说明磁盘满、权限不足或设备异常
结果收集别用 sync.Map,用预分配切片 + 原子索引
分片编号固定(如第 0 片、第 1 片……),sync.Map.Store 或 map + mutex 属于过度设计,写放大明显且无序。
- 主 goroutine 预分配
results := make([]Result, len(chunks)) - 每个子 goroutine 用
atomic.StoreUintptr(&indices[i], 1)或直接写入对应下标(若确保不重叠) - 若需按处理完成顺序收集,改用带缓冲 channel(容量 = 分片数),但仅当真有流式消费需求时才值得引入
*os.File 实例,且该实例不能被任意 goroutine 关闭;Close() 必须在所有 WriteAt 完成后由主 goroutine 统一调用。否则部分写入可能丢失,或触发 bad file descriptor 错误。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











