c.File和c.FormFile不能用于大文件上传、CSV导出等场景,因二者均强制全量载入内存易OOM;真流式须绕过Gin默认解析,直接操作io.Reader/io.Writer,关键在Header前置设置、及时Flush及资源defer关闭。

大文件上传、CSV 导出、SSE 推送、断点续传——这些场景下,c.File 和 c.FormFile 都会把整个内容载入内存,不是真流式,容易 OOM 或阻塞响应。真正可用的流式传输,必须绕过 Gin 默认的 multipart 解析和响应缓冲,直接操作底层 io.Reader / io.Writer。
为什么 c.FormFile 不能用于大文件上传
c.FormFile 内部调用 ParseMultipartForm,强制读取整个请求体到内存,生成 *multipart.FileHeader;哪怕只是想校验文件名或 size,它也已把几百 MB 全 load 进来。一旦并发稍高,GC 压力陡增,runtime: out of memory 很快出现。
- 禁用自动解析:不调用
r.MaxMultipartMemory(),或设为 0(Gin v1.9+ 支持) - 改用
c.Request.MultipartReader(),返回标准multipart.Reader - 每个
part是独立io.Reader,可直接io.Copy(dst, part)到磁盘或对象存储 - 务必检查
part.FormName()是否匹配目标字段名(如"file"),避免误处理文本字段
如何用 io.Copy 流式返回大文件
c.File 是 convenience wrapper,本质仍是 os.ReadFile + Write,不适合 >1MB 文件。流式返回必须手动打开文件、设 Header、再 copy:
- 先
os.Stat检查文件存在且可读,否则open xxx: permission denied会静默失败 -
c.Header("Content-Length", strconv.FormatInt(fi.Size(), 10))必须显式设置,否则浏览器无法显示进度条 -
c.Header("Content-Disposition", "attachment; filename=xxx")中 filename 若含中文,需url.QueryEscape编码 -
defer f.Close()不可省略,否则too many open files错误会在几小时内爆发 - 不支持断点续传——若需
Range处理,得手动解析c.Request.Header.Get("Range")并返回206 Partial Content
CSV 流式导出的关键控制点
用 csv.NewWriter(c.Writer) 是正确起点,但极易因顺序错误导致下载失败或乱码:
立即学习“go语言免费学习笔记(深入)”;
- Header 必须在任何数据写入前设置,
c.Header("Content-Type", "text/csv; charset=utf-8")和c.Header("Content-Disposition", ...)要早于writer.Write(header) -
writer.UseCRLF = true必开,否则 Windows Excel 打开时所有内容挤在第一行 - 每行写完必须调用
writer.Flush(),否则最后一块数据可能滞留在缓冲区不发 - 不要把全部数据 collect 成 slice 再循环——应从数据库 cursor 或 channel 按需获取,保持内存恒定
- 若导出中途出错(如 DB 连接断开),需立即
c.Abort()并关闭 writer,避免客户端收到半截 CSV
SSE 流式推送的响应头与刷新陷阱
SSE(Server-Sent Events)依赖 text/event-stream + 持久连接,Gin 默认会缓冲响应,必须主动干预:
-
c.Header("Content-Type", "text/event-stream")和c.Header("Cache-Control", "no-cache")是底线要求 - 每次写入后必须调用
c.Writer.Flush(),否则事件堆积在 buffer,前端收不到 - Gin 的
c.SSEvent()封装了格式,但底层仍依赖Flush;若手动拼接data: ...\n\n,也得 flush - 连接超时由 HTTP server 控制(如
http.Server.ReadTimeout),非 Gin 层能 override;长连接需配合心跳(:keep-alive注释)防断连 - 注意 goroutine 泄漏:若 client 断开而服务端未感知,需监听
c.Writer.CloseNotify()(已弃用)或改用context.Context.Done()检测
流式传输的复杂点不在代码长度,而在边界控制——哪一步该 flush、哪个 header 必须前置、哪类 error 必须 abort 而非 continue。实际部署时,Content-Length 缺失、Flush 忘记、defer Close() 漏写,这三个问题占线上流式故障的七成以上。


















