调用 req.parsemultipartform(32

直接用 req.ParseMultipartForm() 会卡死进度
这是最常踩的坑:调用 req.ParseMultipartForm(32 后,整个文件先被读进内存或临时磁盘,直到解析完成才进入业务逻辑——进度完全不可见,用户看到的就是“转圈→成功/失败”,中间零反馈。
根本原因是 multipart 解析器必须完整扫描 boundary、提取每个 part 的 header 和 body,无法中断。而大文件(比如 500MB 视频)在解析阶段就可能耗时数秒甚至分钟,且内存占用飙升。
- 正确做法是绕过
ParseMultipartForm,改用req.MultipartReader()拿到原始流 - 再对每个
*multipart.Part的Part.Body套一层自定义io.Reader,在Read()中累加字节数 - 别在 handler 里直接写文件,而是把带计数的
Part.Body传给io.Copy或流式处理逻辑
ProgressReader 必须支持并发安全与节流上报
一个裸的计数 Read() 很容易引发 goroutine 泄漏或 UI 卡顿:每次读几十字节就触发一次回调,高频小消息打满 WebSocket 缓冲区,或者前端反复重绘进度条导致主线程阻塞。
关键不是“要不要计数”,而是“谁来消费计数结果”和“多久发一次”。
-
ProgressReader内部字段如read必须用sync.Mutex或atomic.Int64保护,避免多 part 并发读时统计错乱 - 进度上报不能直连 WebSocket
WriteMessage,应推送到带缓冲 channel(如ch := make(chan Progress, 10)),由独立 goroutine 按阶梯(0% → 10% → … → 100%)或最小增量(如 ≥5% 变化)发送 - 别依赖
Content-Length算百分比——浏览器上传可能不带该 header,尤其拖拽场景;应从Content-Disposition的filename字段 + 客户端预估 size 共同协商总大小
WebSocket 连接必须绑定上传生命周期
很多实现用全局 map[string]*websocket.Conn 存连接,靠 URL 参数 ?upload_id=xxx 查找——但用户刷新页面、重试上传、开多个标签页时,旧连接没关,新进度就推到了已失效的 conn 上,轻则丢消息,重则 panic。
真正可靠的绑定方式是:上传请求和 WS 连接共享同一个上下文或唯一 token,并做双向活跃性校验。
- 上传 handler 启动时,立刻查
wsConnMap[uploadID],确认 conn 的LastPing在最近 30 秒内(需 WS 服务端启用心跳) - 上传结束(无论成功/失败)必须显式调
conn.Close()并从 map 中delete,否则 goroutine 持有 conn 引用,内存泄漏 - 不要复用同一 connection 处理多个上传——每个上传请求生成新 uploadID,对应唯一 WS 连接实例
大文件上传必须设上下文超时与内存限制
没有超时控制的上传接口,可能让恶意客户端慢速上传(如每秒 1 字节)长期占着 goroutine 和 fd,最终拖垮服务。标准库的 http.Server.ReadTimeout 不够用,它只管连接建立阶段,不管 body 读取过程。
必须在 handler 内部用 context.WithTimeout 包裹整个流处理逻辑,并配合 io.LimitReader 防止单个 part 超限。
- 为整个上传设置 10 分钟超时:
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Minute) - 对每个
Part.Body套io.LimitReader(part.Body, maxFileSize),防止某 part 过大撑爆内存 - 用
http.MaxBytesReader包裹原始r.Body,限制整个请求体上限(如 2GB),避免 OOM
ProgressReader,而是让上传 ID、WS 连接、HTTP 请求、后台 goroutine 四者生命周期严格对齐——少一步清理,就埋下泄漏隐患。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











