HTTP服务端正确响应Range请求需用http.ServeContent并传入准确modtime、size及io.ReadSeeker,禁用http.ServeFile;客户端须校验206状态码、安全追加写入并处理代理透传问题。

HTTP 服务端如何正确响应 Range 请求
断点续传的核心是服务端支持 Range 请求头,并返回 206 Partial Content,而不是默认的 200 OK。Go 标准库的 http.ServeContent 函数天然支持这个逻辑,但前提是必须显式传入正确的 modtime 和 size,且文件不能是内存中构造的字节流(比如 bytes.NewReader)。
常见错误是直接用 http.ServeFile ——它虽支持 Range,但会强制读取整个文件 stat 信息,若文件路径不可访问或权限不足,直接返回 404 或 403,且无法自定义 Content-Range 的单位(如必须是 bytes,不能改)。
- 务必用
os.Stat获取真实文件大小和修改时间,传给http.ServeContent - 如果内容来自数据库或加密解密后生成,需先写入临时文件,再通过
os.Open打开——http.ServeContent不接受io.Reader接口,只认io.ReadSeeker(如 *os.File) - 响应头里不要手动写
Content-Range:由http.ServeContent自动设置;手写会导致冲突,触发500 Internal Server Error
客户端如何发起带 Range 的下载请求
Go 客户端实现断点续传,关键不是“怎么发请求”,而是“怎么安全地复用已有文件并计算偏移”。http.Request 本身不处理文件 IO,所有偏移、追加、校验都得自己控制。
典型流程:检查本地文件是否存在 → 用 os.Stat 获取已下载字节数 → 设置 req.Header.Set("Range", "bytes="+strconv.FormatInt(offset, 10)+"-") → 发起请求 → 用 os.OpenFile(..., os.O_WRONLY|os.O_APPEND) 追加写入。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 注意
Range值格式必须是bytes=12345-(末尾带短横),不能是bytes=12345-12345(单字节范围)或bytes=-1024(末尾字节),否则服务端可能忽略或返回416 Range Not Satisfiable - 收到响应后,必须检查状态码是否为
206,不是则说明服务端不支持断点,应清空本地文件重下 - 写入前建议用
f.Seek(offset, io.SeekStart)替代O_APPEND,避免多线程/多进程并发写入时错位(O_APPEND在某些文件系统上不保证原子性)
http.ServeContent 的三个参数为什么不能乱传
http.ServeContent 签名是 func(w ResponseWriter, req *Request, name string, modtime time.Time, size int64, content io.ReadSeeker)。其中 modtime 和 size 不仅用于生成响应头,还直接影响 Range 解析逻辑:
-
size必须等于实际可读总长度;若传小了(比如只传 1MB 但文件实际 2GB),服务端在处理Range: bytes=1000000-时会返回416 -
modtime影响Last-Modified响应头,也参与协商缓存;若设为零值(time.Time{}),Go 会 fallback 到当前时间,导致客户端无法做条件请求(If-Range失效) -
content必须能Seek(0, io.SeekStart)成功;若底层是管道或网络流,Seek返回io.ErrSeeker,整个请求会 panic
如何检测并修复常见的断点续传失败场景
实际部署中,90% 的断点续传失败不是代码逻辑问题,而是环境或协议细节被忽略:
- CDN 或反向代理(如 Nginx)默认不透传
Range请求,会把Range头丢弃并转成完整请求,返回200;需在 Nginx 配置里显式开启proxy_buffering off;和proxy_set_header Range $http_range; - 客户端保存的已下载字节数(offset)若因崩溃未刷盘,重启后从旧 offset 继续,会导致文件中间出现重复或跳变;建议每次写入后调用
f.Sync(),或用临时文件 + 原子 rename - 服务端文件被替换(如热更新二进制包),但
modtime未变,客户端用If-Range检查时认为“文件没变”而继续断点,结果写入内容错位;此时应让服务端对每个资源引入唯一ETag,并在http.ServeContent后手动设置w.Header().Set("ETag", etag)
真正难的不是写几行 Range 代码,而是确认每一层(客户端磁盘、传输链路、代理、服务端文件状态)都严格遵守 HTTP/1.1 的字节范围语义。漏掉任意一环,断点就变成“伪续传”。

















