os.file.seek 是唯一可控的文件指针移动方式,但不校验偏移合法性且不自动适配读写模式,必须用 io.seekstart/current/end 而非数字常量,调用后须检查 err;随机读写优先用线程安全的 readat/writeat;大文件推荐 mmap-go 库按块映射并显式 unmap;断点续传需结合 content-range 或哈希校验,写入后调用 f.sync() 保障落盘。

os.File.Seek 是唯一可控的文件指针移动方式,但它不校验偏移合法性,也不自动适配读写模式差异——直接传错参数或忽略返回值,大概率导致后续 Read 或 Write 读到空数据、跳过内容,甚至 panic。
Seek 必须用 io.SeekStart/Current/End,别手写数字 0/1/2
Go 不做隐式转换,f.Seek(100, 1) 看似省事,实则在不同平台行为不一致(比如某些文件系统对 io.SeekCurrent 的支持有限),且编译器无法帮你发现错误。
- ✅ 正确写法:
f.Seek(100, io.SeekStart)、f.Seek(-10, io.SeekCurrent)、f.Seek(0, io.SeekEnd) - ❌ 错误写法:
f.Seek(100, 1)、f.Seek(0, 2)、f.Seek(-5, io.SeekEnd)(多数文件系统不支持负偏移 +SeekEnd) - 调用后必须检查
err:若返回offset, err := f.Seek(200, io.SeekStart)且err != nil,说明目标位置超出当前文件长度(比如文件只有 150 字节),此时不能继续Read
随机读写优先用 ReadAt/WriteAt,而不是 Seek + Read/Write
并发场景下共享一个 *os.File 时,Seek 后还没 Read,另一个 goroutine 就改了指针,结果读错位置。而 ReadAt 和 WriteAt 是无状态的,不依赖当前文件偏移,天然线程安全。
-
f.ReadAt(buf, offset)返回实际读取字节数,可能小于len(buf)(如到达 EOF),必须检查,不能假设填满 -
f.WriteAt(buf, offset)不会移动内部指针,也不影响其他 goroutine 的写入;但 Windows 上需确保文件以os.O_RDWR打开,否则报权限错误 - 注意:如果文件被其他进程独占锁定(尤其 Windows),
WriteAt会失败,os.IsPermission(err)可辅助判断
大文件随机访问别硬扛,mmap 是更优解但得管好生命周期
标准库 ReadAt 每次都走系统调用+内核缓冲区拷贝,小量跳读开销明显;mmap 让内核按需分页加载,延迟更低。但 Go 标准库不提供跨平台 mmap,直接调 syscall.Mmap 极易出错。
- ✅ 推荐用
github.com/edsrzf/mmap-go:自动处理页对齐、Windows 64KB 对齐限制、错误码转换 - 映射后得到的
mmap.Map值必须显式调.Unmap(),且只能调一次;defer mm.Unmap()在提前 return 时会漏掉,建议用if err != nil { mm.Unmap(); return }配合 - 写入后不调
mm.Flush(),数据只在 page cache 里,断电或崩溃即丢失;频繁 flush 影响性能,可批量写完再刷 - 别一次性 mmap 整个超大文件(如 50GB):虚拟地址空间可能耗尽,缺页中断引发磁盘抖动;应按块映射(如每 64MB 一块),起始 offset 必须页对齐(
offset & 4095 == 0)
断点续传时“已下载部分”是否完整不能只看文件长度
网络中断可能导致最后几个字节写了一半,或磁盘缓存未刷盘。仅靠 file.Stat().Size() 判断已下载长度,容易把损坏片段当有效数据。
- 服务端响应头中的
Content-Range(如bytes 0-999/10000)才是真实已传范围,应与本地文件长度比对 - 更稳妥的是配合分块 hash 或完整文件 MD5 校验,尤其是关键业务场景
- 写入后记得
f.Sync()(对WriteAt有效),但注意它只保证内核缓冲区落盘,不保证磁盘物理写入完成
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











