Ctx.Output.Download不适合大文件下载,因其内部全量读取文件至内存,易导致OOM或超时;需改用http.ServeFile或手动流式传输,并正确设置响应头与分块读写。

Beego 的 Ctx.Output.Download 不适合下载大文件,它会把整个文件读进内存再写入响应体,50MB 以上就容易 OOM 或超时;必须改用流式传输,手动控制读写和响应头。
为什么 Ctx.Output.Download 不能用于大文件
该方法内部调用 ioutil.ReadFile(Beego 1.12 及之前)或类似全量读取逻辑,对 100MB 文件会申请同等大小内存,Web 进程可能被系统 kill;同时未支持 Range 断点续传,也无法设置超时或限速。错误现象包括:浏览器卡住、连接重置、返回空响应但状态码是 200。
- 文件路径存在但返回空内容 → 很可能是内存不足导致 panic 后静默失败
- 服务端日志无报错但客户端收不到数据 →
Download阻塞在读取阶段,未设http.Server.ReadTimeout - 并发下载多个大文件时 CPU/内存飙升 → 全量读取 + 响应体拷贝双重开销
用 http.ServeFile 替代(简单场景)
如果只是提供静态文件下载(如 CDN 源站、后台导出目录),且路径可控、无需鉴权或动态拼接,http.ServeFile 是最轻量的流式方案:它不加载文件到内存,而是用 io.Copy 分块写入,天然支持 Range 和 Content-Length。
- 必须传入绝对路径:
http.ServeFile(c.Ctx.ResponseWriter, c.Ctx.Request.RequestURI, filepath.Abs(filepath.Join("static/downloads", filename))) - 需提前校验
filename,防止路径遍历(如拒绝含../或以.开头的名称) - 不自动设
Content-Disposition,需手动加头:c.Ctx.ResponseWriter.Header().Set("Content-Disposition", "attachment; filename="+filename) - 不支持自定义鉴权逻辑,若需登录态校验,请在调用前做
c.IsLogin或 token 验证
手动流式下载(带鉴权 / 动态源 / 断点续传)
当文件来自远端 URL、数据库 BLOB、或需按用户权限动态生成时,必须自己管理响应流。核心是:禁用 Beego 自动 header 写入、分块 io.Copy、手动处理 Range 头。
- 先检查
Range请求头,若存在且服务端支持(如源站返回Accept-Ranges: bytes),则发带Range的 GET 请求并返回206 Partial Content - 用
bufio.NewReader(resp.Body)+ 固定 buffer(如 32KB)循环io.CopyN到c.Ctx.ResponseWriter,避免一次读太大 - 务必设置
Content-Type和Content-Length(若已知)或Transfer-Encoding: chunked(若长度未知) - 关键:调用
c.Ctx.ResponseWriter.WriteHeader(statusCode)后不能再写 header,所以所有Header().Set()必须在WriteHeader前完成
示例片段:
func (c *MainController) StreamDownload() {
filename := c.GetString("file")
if !isValidFilename(filename) {
c.Abort("400")
return
}
filepath := path.Join("data/uploads", filename)
f, err := os.Open(filepath)
if err != nil {
c.Abort("404")
return
}
defer f.Close()
stat, _ := f.Stat()
c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/octet-stream")
c.Ctx.ResponseWriter.Header().Set("Content-Disposition", "attachment; filename="+filename)
c.Ctx.ResponseWriter.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
c.Ctx.ResponseWriter.WriteHeader(200)
io.Copy(c.Ctx.ResponseWriter, f) // 底层是分块 read+write,不占额外内存
}
中文文件名与兼容性陷阱
IE 和旧版 Edge 不识别 filename*=UTF-8''xxx,只认 filename 字段的 GBK 编码;但 Go 标准库不内置 GBK 编码器。稳妥做法是:服务端统一用英文文件名(如 UUID + 扩展名),前端用 download 属性提示显示名:<a href="/dl?id=abc" download="报告.pdf">下载</a>。
- 若坚持服务端输出中文名,Beego 1.12+ 可用:
c.Ctx.Output.Header("Content-Disposition", "attachment; filename*=UTF-8''"+url.PathEscape("报告.pdf")) - 不要用
filename="报告.pdf"(无编码),会导致乱码或截断 - Chrome/Firefox 支持
filename*,Safari 15.4+ 才开始支持,低版本 Safari 会忽略整条头
真正难处理的是「大文件 + 中文名 + 旧 IE」组合——这种场景建议放弃直连下载,改为跳转到中间页生成临时英文链接,再 302 重定向。


















