Go中无直接写偏移操作,必须先Seek再Write或用WriteAt;混用bufio.Writer与Seek会导致偏移错乱,Seek是内核维护的唯一可靠入口。

Go 里没有“直接写到偏移量”的单步操作,必须显式调用 Seek 定位,再用 Write 或 WriteAt;混用 bufio.Writer 和 Seek 会导致偏移错乱,这是最常踩的坑。
Seek 后 Write 是最常用也最容易出错的方式
文件写入位置由内核维护,Seek 是唯一可靠入口。常见错误包括:
- 打开文件时漏掉
os.O_RDWR—— 只读模式下Seek成功但后续Write报bad file descriptor - 误用数字代替
io.SeekStart(比如传0)—— 虽然当前有效,但可读性差、易被重构误改 -
Seek返回值未检查:偏移超出文件末尾时仍返回新位置,但后续Write可能触发稀疏写(中间补零),而非报错 - 在
bufio.Writer包裹的文件上Seek,缓冲区未刷新,导致写入位置和预期脱节
正确流程是:file.Seek(offset, io.SeekStart) → 检查 err → file.Write(data)。若需并发安全,别走这路。
WriteAt 不依赖当前偏移,但不更新指针也不扩展文件
WriteAt 是真正的“无状态”写入,每次调用都独立指定位置,适合单线程写固定结构体或头信息。
立即学习“go语言免费学习笔记(深入)”;
- 它不改变文件当前 offset,所以不能和后续
Write混用 —— 下一次Write仍从老位置开始 - 若
offset + len(data) > file.Stat().Size(),会返回io.EOF,不会自动扩展文件长度 - 想写到末尾并扩展文件?得先
Seek(0, io.SeekEnd)再Write,或手动Truncate到目标大小 - 并发写同一区域时行为未定义,不加锁就用
WriteAt等于埋雷
典型用途:覆盖二进制 header、写数据库页、填充已预分配的文件空洞。
HTTP 流式响应写入指定偏移要绕过内存缓冲
分片下载或多源合并场景下,绝不能用 io.ReadAll 或 bytes.Buffer 中转,否则内存随响应体积线性增长。
- 必须先
file.Seek(offset, io.SeekStart),再用io.Copy(file, resp.Body) -
io.Copy内部用固定 32KB 栈缓冲,全程零堆内存堆积 - 注意
resp.Body必须被完整读完(哪怕只读一部分也要io.Copy(io.Discard, ...)),否则连接可能复用失败 - 若写入中途出错,文件指针已移动,需手动
Seek回原位再处理,否则后续写入错位
这个组合是大文件拼接的底线方案,比任何封装都轻量可控。
别信手动累加,Seek(0, io.SeekCurrent) 才是唯一真相
有人试图靠 len(data) 累加推算当前位置,这在 bufio.Scanner、ReadLine、甚至底层 syscall 分片时都会失效。
-
ReadLine剥离换行符,你加+2就错;最后一行没换行,又少算 -
bufio.Reader的缓冲区会让实际读取位置和逻辑位置不一致 - 唯一能拿到真实偏移的方法只有:
offset, err := file.Seek(0, io.SeekCurrent) - 这个调用不移动指针,只返回从文件开头起的绝对字节数,且要求
file是*os.File(非bufio封装)
真正容易被忽略的是:偏移量是内核状态,不是 Go 运行时自己维护的。一旦混用缓冲层和底层句柄,错位就不可逆。


















