Go中真随机写需先用os.File.Seek定位再Write;Write总是追加,WriteAt不扩容文件,超长写入会被截断。

Go 中用 os.File.Seek 定位再写入才是真随机写
Go 没有“随机写入”专用函数,所谓随机写,本质是先用 os.File.Seek 跳到任意偏移位置,再调用 Write 或 WriteAt。直接调用 Write 总是从当前文件指针位置追加——哪怕你刚 OpenFile 且没读过,指针也在开头,但一旦写过一次,指针就移到末尾了,后续不 Seek 就没法回头写。
常见错误现象:Write 总是追加、老写在文件末尾;或 WriteAt 返回成功但文件没变长,内容也不见——多半是没提前扩容文件(比如用 Truncate)。
-
Seek(offset, io.SeekStart)最常用:从文件开头算起,offset必须 ≥ 0 -
Seek(0, io.SeekEnd)可快速定位到末尾,等价于Stat().Size - 如果目标 offset 超出当前文件长度,
Write不会自动填充空洞(sparse file 行为依赖 OS 和文件系统),多数情况需先Truncate到目标长度
WriteAt 看似简单,但只适用于已知长度的固定偏移写入
WriteAt 把 Seek + Write 合成一步,但它不会改变文件长度:若 offset + len(p) 超过当前大小,写入会被截断(返回实际写入字节数
使用场景:日志分片覆盖、数据库 WAL 日志页写入、内存映射式缓存更新。
立即学习“go语言免费学习笔记(深入)”;
-
WriteAt([]byte("hi"), 1024)—— 仅当文件长度 ≥ 1024 + 2 才能完整写入 - 想确保写入成功?先
Stat拿当前大小,再按需Truncate - 注意:Windows 下对普通文件调用
WriteAt可能返回ENOTSUP,建议优先用Seek + Write
处理空洞(hole)和文件扩容必须手动干预
Go 不提供“自动填充空洞”的机制。比如你想往偏移 1MB 处写 4 字节,但文件只有 1KB,不扩容就写不进去——Seek 成功,Write 却可能返回 0, nil(Linux)或报 io.ErrUnexpectedEOF(某些情况)。
正确做法是显式扩容:
- 用
file.Truncate(size)把文件拉长到至少offset + len(data) - 扩容后,新增区域内容是未定义的(通常为 \x00,但不保证),别假设它是零
- 如果只是覆盖中间某段,且确定文件足够长,可跳过
Truncate,但务必Stat校验 - 频繁随机写小块数据时,避免每次
Truncate——可预分配大文件,或用os.O_CREATE | os.O_TRUNC初始化一次
并发随机写必须加锁,os.File 本身不线程安全
os.File 的 Write、WriteAt、Seek 都不是原子操作。多个 goroutine 直接共用一个 *os.File 并发写,极大概率出现指针错乱、覆盖错位、写入丢失。
解决方式只有两种:
- 每个 goroutine 自己
Open文件(开销大,但最安全,适合低频写) - 共用一个
*os.File,但用sync.Mutex或sync.RWMutex包裹Seek+Write组合操作(高频写推荐) -
WriteAt是线程安全的(因为不依赖内部指针),但前提是 offset 计算无竞态——所以 offset 仍需外部同步
容易被忽略的是:即使只读不写,多个 goroutine 对同一 *os.File 并发 Seek + Read 也会相互干扰。只要共享 *os.File,就必须考虑同步。


















