http handler 默认有30秒超时且响应结束后连接即断,无法支撑分钟级下载;直接在handler中执行会阻塞goroutine、拖垮并发,并易被nginx等反向代理中断。

为什么不能直接用 HTTP handler 做长时下载
Go 的 http.Handler 默认有超时限制(通常 30 秒),且响应流一旦结束,连接就断开。后台静默下载意味着:文件拉取可能耗时数分钟、不阻塞用户界面、完成后要主动触达用户(比如发 WebSocket 消息或写数据库状态)。直接在 handler 里 http.Get + io.Copy 会卡住 goroutine、拖垮并发能力,还容易被反向代理(如 Nginx)中断。
用独立 goroutine + context 控制下载生命周期
启动下载任务必须脱离 HTTP 请求生命周期,但又得可控——不能让失败任务无限堆积,也不能让重启后任务丢失。推荐结构:
- 收到请求后,生成唯一
taskID(如 UUID),存入内存 map 或 Redis,状态设为"pending" - 用
context.WithTimeout包裹下载逻辑,避免 goroutine 泄漏 - 下载成功后更新状态为
"done",并触发通知(如调用notifyUser(taskID, userID)) - 失败时设为
"failed",记录错误日志(err.Error())
示例关键片段:
go func() {
defer func() { recover() }() // 防 panic 崩溃整个服务
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
store.UpdateTaskStatus(taskID, "failed", err.Error())
return
}
defer resp.Body.Close()
// ... 写入本地文件或对象存储
store.UpdateTaskStatus(taskID, "done", "")
notifyUser(taskID, userID)
}()
通知用户时别依赖 HTTP 连接残留
用户发起下载请求后早就关闭了页面,HTTP 响应早已结束。所谓“通知”,本质是异步消息投递。常见可靠做法:
- WebSocket:服务端通过
conn.WriteMessage推送 JSON,前端监听"download_complete"事件 - 轮询 API:前端定时 GET
/api/task/status?task_id=xxx,后端返回{"status":"done","file_url":"/downloads/xxx.zip"} - 邮件/短信:适合大文件或非实时场景,调用第三方 SDK(如 SendGrid)
注意:WebSocket 连接需维护 session 关联,不能只靠 userID,建议在建立连接时存 map[userID][]*websocket.Conn,推送前检查 conn 是否 WriteDeadline 未过期。
文件存储路径和清理不能硬编码
静默下载的文件若直接写到 ./tmp/,重启服务就丢,且多实例部署时无法共享。必须解耦:
- 优先存对象存储(如 S3 兼容服务),用
minio.Client.PutObject,URL 直接返回给用户 - 若必须本地存储,路径需可配置(如
DOWNLOAD_DIR="/data/downloads"),并加定时清理(filepath.WalkDir扫描 >24h 的文件) - 文件名别用原始
Content-Disposition,易含非法字符;改用fmt.Sprintf("%s_%d.zip", taskID, time.Now().Unix())
另外,os.OpenFile 必须带 os.O_CREATE|os.O_WRONLY|os.O_TRUNC,否则并发写同一文件会出错。
真正麻烦的是任务重试和幂等性——用户点两次“下载”,得识别重复请求,靠 taskID 去重,而不是反复拉取源文件。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











