writeat要求文件可读写,不自动创建或截断,偏移越界可能产生空洞,不改变文件指针,不保证写满,需检查返回值,适合循环日志等场景。

WriteAt 写入时文件必须是可读写的
Go 的 os.File.WriteAt 不会自动创建或截断文件,它只负责把字节写到指定偏移位置。如果打开文件时用了 O_RDONLY(比如 os.Open),调用 WriteAt 会直接返回 io.ErrPermission 错误。
正确做法是用 os.OpenFile 显式带上 os.O_RDWR 或 os.O_WRONLY 标志:
file, err := os.OpenFile("data.bin", os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
log.Fatal(err)
}
defer file.Close()
n, err := file.WriteAt([]byte("hello"), 1024) // 写到第 1024 字节处
- 即使只写几个字节,文件大小也不会自动扩展——如果偏移量超出当前文件长度,
WriteAt仍会成功,但中间空白区域会被填充为零字节(取决于底层文件系统行为,POSIX 允许“空洞文件”) - Windows 上某些文件系统(如 FAT32)不支持空洞,写入越界偏移可能失败;Linux ext4、XFS 通常支持
WriteAt 不会改变文件当前读写位置
这和 Write 不同:Write 会推进文件的内部偏移指针,而 WriteAt 是纯偏移寻址操作,调用前后 file.Seek(0, io.SeekCurrent) 返回的值不变。
这意味着你可以反复对同一文件句柄调用 WriteAt 写不同位置,互不影响:
file.WriteAt([]byte("abc"), 0) // 位置 0
file.WriteAt([]byte("xyz"), 100) // 位置 100
// 此时文件指针仍在初始位置(通常是 0),不影响下一次 WriteAt
- 如果你之后混用
Write和WriteAt,要注意逻辑顺序——Write的行为取决于当前指针位置,而WriteAt完全无视它 - 并发调用多个
WriteAt是安全的(因为无状态),但多个 goroutine 同时写同一偏移区域会导致数据覆盖,需自行加锁
WriteAt 返回的 n 值可能小于预期长度
WriteAt 的签名是 func (f *File) WriteAt(b []byte, off int64) (n int, err error),它不保证一次性写完全部字节。虽然在本地磁盘文件上极少发生,但在某些特殊文件(如设备文件、FUSE 挂载点、网络文件系统)中,可能只写入部分数据就返回。
务必检查返回的 n 是否等于 len(b):
b := []byte("data")
n, err := file.WriteAt(b, 4096)
if err != nil {
log.Fatal(err)
}
if n != len(b) {
log.Printf("short write: wrote %d of %d bytes", n, len(b))
// 这里通常应重试或报错,不能假设写入完成
}
- 标准库的
io.WriteAt接口不要求实现必须写满,所以不能跳过这个判断 - 如果业务要求原子写入(比如固定长度结构体),建议封装一个
MustWriteAt辅助函数,循环直到写满或出错
用 WriteAt 实现日志文件的循环覆盖写入
一种典型场景是固定大小日志文件(如 1MB),写满后从头覆盖。这时不能依赖 Truncate 清空,而是用 WriteAt 配合模运算定位:
const maxSize = 1
- 注意:这种方案要求文件一开始就被
Truncate到maxSize,否则WriteAt越界写入会产生稀疏文件,且首次写入前必须确保文件存在并有足够空间 - 多进程共享同一日志文件时,
WriteAt本身不提供同步,需配合文件锁(flock)或独立协调机制
偏移计算和边界检查容易出错,尤其是涉及并发更新 offset 时——漏掉原子操作或忘记取模,就会导致写飞或覆盖关键数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











