beego 中 savetofile 同步写入会阻塞 http 请求,正确做法是校验后立即返回任务 id,用后台 goroutine 流式处理文件、记录状态并清理临时资源,下载时需显式设置 content-disposition 头。

Beego 中直接用 SaveToFile 处理大文件会阻塞 HTTP 请求
Beego 的 SaveToFile 是同步写入,调用时会一直占用当前 goroutine 和 HTTP 连接,直到文件写完。对几百 MB 以上的文件,这会导致超时、内存暴涨、甚至整个服务响应变慢。
真正可行的做法是:接收上传请求后,立即返回任务 ID,把保存/打包逻辑扔进后台 goroutine 执行,并用 context 控制超时(比如 15 分钟)。
- 上传接口只校验文件名、Content-Length、MIME 类型,不读取 Body 全体
- 用
f.GetFile("file")拿到multipart.File后,立刻defer file.Close(),但不要调SaveToFile - 把
file和元数据传给后台 goroutine,用io.Copy流式写入磁盘或对象存储,避免全量加载 - 记录任务状态到数据库或内存 map(如
map[string]string{"task_abc": "processing"}),供后续轮询查状态
异步打包完成后如何可靠通知前端?别依赖已关闭的 HTTP 连接
用户点击“开始打包”后页面可能已刷新或关闭,此时 HTTP 响应早已结束。notifyUser() 如果还试图往原始 f.Ctx.ResponseWriter 写东西,会 panic 或静默失败。
可靠通知只有三种落地方式,按实时性排序:
-
WebSocket 推送:需在用户登录/进入页面时建立连接,并将
*websocket.Conn绑定到userID或sessionID;推送前检查conn.WriteDeadline是否过期,否则先conn.SetWriteDeadline -
前端轮询 API:提供
GET /api/task/status?task_id=xxx,后端返回{"status":"done","download_url":"/downloads/xxx.zip"};注意加缓存控制头(Cache-Control: no-cache),避免浏览器缓存旧结果 - 邮件/SMS:适合超大文件(>2GB)或离线场景,调用 SendGrid、阿里云短信 SDK 等,失败要重试 + 记日志
静态资源路径配置不当导致下载链接 404 或被直接渲染
Beego 默认不自动处理文件下载头,即使你把 ZIP 放进 ./staticfiles 并用 web.SetStaticPath("/staticfiles", "./staticfiles") 暴露,浏览器仍可能尝试打开 ZIP 而非下载——因为响应缺少 Content-Disposition: attachment; filename="xxx.zip"。
正确做法不是靠静态路径直出,而是用控制器显式设置头:
func (c *FileController) Download() {
taskID := c.GetString("task_id")
filePath := "/path/to/" + taskID + ".zip"
f, err := os.Open(filePath)
if err != nil {
c.Abort("404")
return
}
defer f.Close()
c.Ctx.ResponseWriter.Header().Set("Content-Type", "application/zip")
c.Ctx.ResponseWriter.Header().Set("Content-Disposition", `attachment; filename="`+taskID+`.zip"`)
c.Ctx.ResponseWriter.Header().Set("Content-Transfer-Encoding", "binary")
c.Ctx.ResponseWriter.Header().Set("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
io.Copy(c.Ctx.ResponseWriter, f)
}
注意:Content-Length 必须设对,否则 Chrome 可能显示“已损坏”,且不能和 gzip 中间件共用(Beego 默认不 gzip 二进制流,但若全局启用了压缩中间件,需排除 /download/* 路由)。
并发打包多个大文件时,临时目录和磁盘空间容易爆掉
Beego 默认把上传文件暂存在内存或系统临时目录(/tmp),如果同时有 10 个用户各上传 500MB 文件,/tmp 很快写满,os.TempDir() 返回的路径也未必可写。
关键控制点:
- 在
conf/app.conf中显式设置maxmemory = 1(1MB),强制大文件走磁盘,避免吃光内存 - 用
web.MaxUploadSize = 1073741824(1GB)限制单次上传上限,防止恶意上传耗尽磁盘 - 后台打包 goroutine 完成后,必须主动清理临时文件:
os.Remove(filePath),并捕获os.IsNotExist错误 - 生产环境建议把打包输出目录挂载为独立磁盘分区,用
df -h定期检查剩余空间,低于 10% 时拒绝新任务
临时文件没删干净、磁盘满了却不报错、通知逻辑里忘记检查连接是否还活着——这些细节比打包逻辑本身更容易让整个流程卡死。











