直接用http.get无法断点续传,因为其默认不发送range头,服务端视为全新请求返回完整文件;断点续传必须手动构造带range头的请求,校验accept-ranges支持、206响应及content-range,并用os.o_rdwr+seek精准写入。

为什么直接用 http.Get 无法断点续传
因为 http.Get 默认不发 Range 头,服务端就当全新请求处理,返回完整文件 —— 即使本地已下了一半,也会重头开始。断点续传本质是靠 HTTP 的 Range 请求头 + 服务端支持 206 Partial Content 响应实现的。
常见错误现象:io.Copy 写入已有文件时直接覆盖、或追加但没跳过已下载部分,导致文件损坏;更隐蔽的是,某些 CDN 或 Nginx 默认关闭 Accept-Ranges,即使你发了 Range,对方仍返回 200 OK 全量内容,你以为续传了,实际在重复写。
- 务必先 HEAD 请求检查响应头是否含
Accept-Ranges: bytes - 用
os.OpenFile以os.O_WRONLY | os.O_CREATE | os.O_APPEND打开文件,但注意:追加模式不能跳过已有字节,必须用os.O_RDWR+file.Seek - 如果服务端不支持
Range(返回200),应 fallback 到完整下载,而非强行续传
如何构造带 Range 头的请求并校验响应
核心是手动构建 *http.Request,设置 Header.Set("Range", "bytes=1024-"),然后检查响应状态码和头信息。不要依赖第三方库自动处理,容易掩盖服务端不兼容的问题。
示例关键逻辑:
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", offset))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusPartialContent {
// 可安全续传
} else if resp.StatusCode == http.StatusOK {
// 服务端无视 Range,需清空文件重下
os.Truncate(filename, 0)
offset = 0
} else {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
-
offset必须是本地文件当前长度(stat.Size()),不是“想从哪下”,否则会跳过或重复 - 某些服务端对
Range格式敏感,比如不支持bytes=-1024(末尾 N 字节),只认bytes=N-或bytes=N-M - 务必检查
Content-Range响应头里的总大小(如bytes 1024-4095/8192),用于更新预期总长
如何安全写入并避免并发覆盖
续传时文件句柄要复用,且写入前必须 file.Seek(offset, io.SeekStart),否则 io.Copy 会从文件开头或末尾(取决于打开方式)写,破坏数据。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
多 goroutine 并发下载同一文件?别这么做。单文件续传本身是顺序 I/O,加锁或 channel 控制反而引入复杂度。真要并发分片下载,得自己拆 Range 区间、分别请求、再按偏移合并,那是另一个问题。
- 用
os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644),不是O_APPEND -
file.Seek(offset, io.SeekStart)后再io.Copy,确保写入位置准确 - 写入完成后调用
file.Sync(),防止内核缓存未刷盘导致下次续传读到脏长度 - 临时文件名(如
filename.part)比直接操作目标文件更安全,成功后再os.Rename
怎么判断下载完成和校验完整性
HTTP 层面没有“下载完成”事件,只能靠 Content-Length 响应头(全量下载时)或 Content-Range 中的总长度(续传时)与本地文件大小比对。但这两者都不可信 —— 服务端可能动态生成内容,Content-Length 不准确。
真正可靠的结束条件只有两个:io.Copy 返回 EOF,且写入字节数等于预期(来自 Content-Range 或 Content-Length);或者你主动根据总长计算出最后一块的 Range,收到的字节数刚好匹配。
- 每次写入后用
file.Stat().Size()获取实时大小,别缓存初始offset - MD5/SHA256 校验必须在下载完成后独立进行,不能边下边算(除非用
hash.Hash包装io.MultiWriter) - 如果服务端不返回
Content-Range(比如只返回200),就无法知道总长,只能靠Content-Length或流式读取直到 EOF
最麻烦的其实是服务端行为不一致:有些返回 206 但 Content-Range 里总长是 0,有些对空 Range 头返回 200,有些根本没 Accept-Ranges 却假装支持。实际写的时候,得把每种响应都当异常 case 处理,而不是假设标准流程一定走通。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










