Beego原生Download不支持断点续传,因其不解析Range头、不返回206状态码及Content-Range头;需手动处理Range请求,用os.File.Seek分段读取并设置正确响应头。

Beego 中 Controller.Ctx.Output.Download 不支持断点续传
Beego 原生的 Download 方法本质是读取文件后调用 Write 一次性输出,它不会解析 Range 请求头,也不设置 Accept-Ranges、Content-Range 等响应头,因此浏览器或下载工具发起断点续传请求时,服务端直接返回完整文件(状态码 200),导致客户端认为“上次下载内容被覆盖”,续传失败。
常见错误现象包括:curl -r 1000-2000 -o part.bin http://x/file.zip 返回 200 而非 206;手机迅雷/IDM 显示“不支持断点”;iOS Safari 下载大文件中途刷新后从头开始。
- 必须手动处理
Range请求头并返回206 Partial Content - 需显式设置
Content-Range、Accept-Ranges: bytes和Content-Length(注意:不是整个文件大小) - 不能复用
Download,要绕过它,直接操作Controller.Ctx.ResponseWriter
手动实现断点续传:读取文件分段并设置正确响应头
核心逻辑是检查 Range 头,计算起始偏移、长度,用 io.CopyN 或 io.ReadAt 安全读取对应区间。Beego 的 Context 没有封装 Seek 支持,所以推荐用 os.File 自行打开并 Seek。
示例关键代码片段:
func (c *FileController) Download() {
filePath := "/path/to/large.zip"
f, err := os.Open(filePath)
if err != nil {
c.Ctx.Abort(404, "file not found")
return
}
defer f.Close()
<pre class="brush:php;toolbar:false;">fi, _ := f.Stat()
size := fi.Size()
rangeHeader := c.Ctx.Request.Header.Get("Range")
if rangeHeader == "" {
// 无 Range,按普通下载处理(可选 302 重定向到静态路径,或继续走这里)
c.Ctx.ResponseWriter.Header().Set("Content-Disposition", `attachment; filename="large.zip"`)
c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/zip")
c.Ctx.ResponseWriter.Header().Set("Accept-Ranges", "bytes")
http.ServeContent(c.Ctx.ResponseWriter, c.Ctx.Request, "", fi.ModTime(), f)
return
}
// 解析 Range: bytes=1000-1999
var start, end int64
if _, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); err != nil {
c.Ctx.Abort(416, "Requested Range Not Satisfiable")
return
}
if start = size || start > end {
c.Ctx.Abort(416, "Requested Range Not Satisfiable")
return
}
length := end - start + 1
c.Ctx.ResponseWriter.WriteHeader(206)
c.Ctx.ResponseWriter.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, size))
c.Ctx.ResponseWriter.Header().Set("Accept-Ranges", "bytes")
c.Ctx.ResponseWriter.Header().Set("Content-Length", strconv.FormatInt(length, 10))
c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/zip")
c.Ctx.ResponseWriter.Header().Set("Content-Disposition", `attachment; filename="large.zip"`)
f.Seek(start, 0)
io.CopyN(c.Ctx.ResponseWriter, f, length)
}
-
http.ServeContent可以复用(它本身支持 Range),但要求传入的ReadSeeker是可 seek 的,*os.File满足;注意它会自动处理 304、If-Modified-Since 等,若不需要可不用 - 务必校验
start和end范围,越界必须返回416,否则可能触发 panic 或泄露文件尾部填充数据 - 不要用
c.Data["xxx"]或c.TplName,它们和文件流输出冲突
生产环境要注意的几个实际问题
本地测试通不代表线上可用。Beego 运行在 Nginx 后面时,Nginx 默认会缓存整个响应体,并忽略上游的 206 和 Content-Range,导致所有请求都变成 200 —— 这是最容易被忽略的部署级坑。
- Nginx 需显式开启
underscores_in_headers on;(如果自定义 header 用了下划线) - 确认 Nginx 没有配置
proxy_buffering on或proxy_cache,这些会吞掉分块响应;建议加proxy_buffering off;和proxy_cache off; - 大文件长时间连接下,Beego 默认的
MaxMemory(用于 multipart 解析)不影响下载,但 Go HTTP Server 的ReadTimeout/WriteTimeout要调大,否则连接可能被意外中断 - 并发下载多段时,
*os.File是共享的,Seek+Read是线程安全的,无需额外加锁
替代方案:用 Nginx 原生处理更可靠
如果业务中下载路径固定、文件不常变,最稳的方式是让 Nginx 直接服务静态文件,并开启 sendfile on 和 tcp_nopush on。Nginx 对 Range 的支持成熟且高效,Beego 完全不参与文件 I/O。
只需在 Beego 路由里做权限校验,然后 c.Ctx.Redirect(302, "/static/xxx.zip"),再配 Nginx 的 location /static/ 指向磁盘路径,并确保 disable_symlinks 和权限控制到位。
这样既规避了 Go 层读文件的内存与 GC 压力,又避免了框架层对 HTTP 协议细节的重复实现。真正需要 Beego 动态生成或鉴权后才允许下载的场景,才值得投入精力手写 Range 支持。











