c.File()在大文件场景下必然OOM,因其底层调用http.ServeFile,会将整个文件一次性加载进内存再发送,100MB文件即占100MB堆内存,100并发时至少消耗10GB内存,直接触发runtime: out of memory;且不支持断点续传、无法设自定义MIME、权限校验滞后并静默返回404/500。

为什么c.File()在大文件场景下必然OOM
c.File()底层调用http.ServeFile,会把整个文件读进内存再发出去。100MB文件 → 100MB堆内存;100并发 → 至少10GB内存占用。这不是慢,是直接触发runtime: out of memory。更糟的是,它不支持断点续传、无法设自定义MIME、权限校验滞后——文件已开始读取时才检查路径/权限,出错就静默返回404或500,日志里根本没线索。
必须用c.DataFromReader,且size参数不能估
c.DataFromReader不是“替代方案”,是生产环境唯一可行的下载方式。但它强制要求Content-Length头与实际字节数严格一致,否则iOS Safari拒载、Chrome进度条卡死、断点续传失效。
- 磁盘文件:用
os.Stat().Size(),别用len(buf)或手动计算 - 生成式流(如动态CSV):必须提前知道总大小;若做不到,得放弃
c.DataFromReader,改用chunked编码+手动写header - 底层
io.Reader不支持io.Seeker(比如gzip.NewReader):size错会导致截断,且无任何错误提示
中文文件名必须用url.PathEscape,不是QueryEscape
Content-Disposition里的filename=字段只接受URI path编码。用url.QueryEscape会把空格转成+,Chrome解析失败后回退到乱码名;而url.PathEscape生成%20,各浏览器兼容。
正确写法:c.Header("Content-Disposition", "attachment; filename*=UTF-8''" + url.PathEscape("报告.pdf"))。注意filename*=语法,别漏掉UTF-8''前缀。
立即学习“go语言免费学习笔记(深入)”;
流式传输时响应头必须在Write前全部设完
c.DataFromReader一旦开始写入body,就再也不能改header。常见坑是权限校验放错位置——比如先c.File()再校验,或在校验后又追加c.Header(),结果header被忽略,Content-Length缺失,浏览器直接中断连接。
- 顺序必须是:校验权限 → 设所有header(
Content-Type、Content-Disposition、Content-Length) → 调c.DataFromReader - 别在
DataFromReader之后调c.Abort()或c.String(),那会panic - 如果需要记录下载行为,必须在
DataFromReader之前完成,比如写DB或发MQ
Content-Length不准、header设晚、中文名编码错——这三个点任何一个出问题,都会导致前端表现诡异,但服务端日志完全沉默。


















