断点恢复必须用 seek()+o_wronly+显式偏移控制,禁用o_append;元数据需落盘并sync;重启时须校验文件长度、修改时间及是否同文件系统。

os.O_APPEND 是断点恢复最常踩的坑,它会让所有 Write() 忽略 Seek(),强制追加到末尾——结果就是文件中间出现大片零字节,校验失败。真正能恢复的断点,必须靠 Seek() + O_WRONLY + 显式偏移控制。
如何安全读取并应用断点偏移量
断点信息不能只存在内存里,进程一挂就全丢。必须落盘为独立元数据文件(如 .meta),且每次写入后立即 file.Sync() 或 fsync()。
- 用
os.OpenFile(metaPath, os.O_RDWR|os.O_CREATE, 0644)打开元数据文件,避免O_TRUNC清空已有记录 - 读取前先
metaFile.Seek(0, io.SeekStart),再io.ReadFull(metaFile, buf),别用Read()配合len(buf)判断——可能只读了部分字节 - 解析出的偏移量必须用
strconv.ParseInt()转为int64,不能用atoi或直接类型断言 - 读取源文件长度时,必须调用
srcStat, _ := os.Stat(srcPath),再比对offset >= srcStat.Size()——若已超长,说明源文件被删或截断,应清空目标文件重来
打开目标文件时为什么不能用 os.O_APPEND
os.O_APPEND 和 Seek() 是互斥的。哪怕你先 f.Seek(1024, io.SeekStart) 成功返回 1024,下一次 f.Write(data) 仍会跳到文件末尾写入,导致数据错位。
- 正确打开方式:
f, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE, 0644) - 打开后立刻
n, err := f.Seek(offset, io.SeekStart),并检查n == offset;不等说明文件已被外部修改,不可续传 - 写入前建议先
f.Truncate(expectedTotalSize),防止稀疏文件后续读取返回零字节 - 绝对不要用
bufio.Writer包裹该文件句柄——缓冲层会破坏Seek与实际落盘位置的对应关系
服务端 Range 支持必须实测,不能信响应头
Accept-Ranges: bytes 只是服务端“声称”支持,Nginx、CDN 常伪造它。真正有效的 Range 支持,必须发试探请求验证行为。
- 发一个
GET /file.zip请求,带Range: bytes=0-1023头 - 仅当同时满足以下三点才启用续传:
resp.StatusCode == http.StatusPartialContent、resp.Header.Get("Content-Range") != ""、解析出的总长度(如bytes 0-1023/12345678中的12345678)与HEAD请求拿到的Content-Length一致 - 若返回
416 Requested Range Not Satisfiable,不是失败信号——可能是请求范围超过文件真实大小,此时应降级为HEAD获取真实Content-Length后重试 - 阿里云 OSS 等对象存储对小于 1MB 的文件可能不返回
Content-Range,但依然支持 Range,需单独适配:收到 206 且无Content-Range时,按Content-Length推算总大小
中断后重启时最易忽略的校验点
很多实现能跑通“正常流程”,但一遇中断就崩溃或静默损坏,问题往往出在重启时没做这几件事:
- 检查本地目标文件是否比元数据记录的偏移量更短——比如磁盘满导致最后几块没写成功,但
.meta已更新,此时应把文件Truncate()回元数据偏移量位置 - 对比源文件
ModTime()和上次记录的值,若被修改过,说明内容已变,必须清空重下 - 用
os.SameFile()检查目标文件和临时文件是否在同一文件系统——否则os.Rename()会失败,得 fallback 到拷贝+删除 - 并发写同一文件时,
sync.Mutex必须只保护元数据更新,绝不能包裹f.Write()——I/O 阻塞会导致死锁
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











