应直接使用 http.ServeContent 处理大文件下载,因其自动支持 Range 请求、断连检测和零拷贝;禁用 Iris 中间件、传入原始 *os.File、显式设置 Content-Disposition 和准确 ModTime 是三大必要条件。

直接用 http.ServeContent,别手写 io.Copy;Iris 本身不接管文件传输逻辑,但必须绕过中间件、正确设置响应头、确保底层 *os.File 可 seek。
为什么不能用 io.Copy 实现大文件下载
io.Copy 看似简单,实际会绕过所有 HTTP 协议层优化:它不处理 Range 请求(断点续传失效)、忽略 If-Modified-Since 和 ETag(缓存完全不可用)、无法触发内核零拷贝(sendfile 被协议封装拦死)。实测中,1GB 文件用 io.Copy 下载吞吐常卡在 80–120 MB/s,而 http.ServeContent 在相同机器上可达 1.2 GB/s(Linux + ext4 + noatime)。
常见错误现象:
- 前端点击下载后进度条卡住,F5 刷新才开始动
- 手机浏览器下载一半失败,重试时从头开始而非续传
- P99 延迟突增,GC pause 频繁超过 15ms
必须绕过 Iris 中间件和路由封装
Iris 的 ctx.ResponseWriter() 是包装过的对象,不满足 http.ServeContent 对底层连接的控制要求。若注册为普通路由 handler(如 app.Get("/download", handler)),请求会经过日志、CORS、JWT 等中间件,而中间件里一旦调用过 ctx.ReadBody() 或 ctx.PostValues(),就会消费掉 request body 流——这对下载接口虽无直接影响,但会额外分配内存、拖慢初始化。
正确做法是用原生 http.ServeMux 或直接挂到 app.WrapRouter() 后的裸 http.Handler 上:
- 禁用
app.HandleDir或iris.FileServer,它们自带中间件链 - 改用
http.StripPrefix("/download", http.FileServer(http.Dir("/data/files"))),再用app.Any("/download/{*filepath:path}", iris.FromStd(...)) - 或更干净:启动前单独起一个
http.Server处理下载路径,与 Iris 主服务共用端口但分离逻辑
http.ServeContent 的三个硬性条件
http.ServeContent 要生效零拷贝,三者缺一不可:
- 源对象必须实现
io.ReadSeeker和Stat()——*os.File天然符合,但bytes.Buffer、加密 reader、gzip reader 都不行 - 必须显式设置
w.Header().Set("Content-Disposition", "attachment; filename=\"xxx.pdf\""),否则浏览器可能尝试渲染 PDF/HTML 而非下载 - 必须传入准确的
modtime:调用时用fi.ModTime(),不能用time.Now()或省略;否则If-Modified-Since永远不命中 304
示例关键片段:
func serveFile(w http.ResponseWriter, r *http.Request) {
path := "/data/files/" + strings.TrimPrefix(r.URL.Path, "/download/")
f, err := os.Open(path)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
defer f.Close()
fi, _ := f.Stat()
w.Header().Set("Content-Disposition", `attachment; filename="`+filepath.Base(path)+`"`)
http.ServeContent(w, r, filepath.Base(path), fi.ModTime(), f)
}
大文件场景下还要注意的细节
即便满足上述条件,生产环境仍容易翻车:
- 文件路径拼接必须用
filepath.Join,避免../路径穿越(Iris 默认不校验,http.ServeContent也不拦截) - 并发高时,
os.Open可能触发大量 syscalls,建议提前用os.Stat检查存在性并缓存modtime,减少重复 open - HTTP/2 必须启用 TLS 才能工作,而
http.ServeContent在 HTTP/2 下对 header 压缩(HPACK)友好,但不要给二进制文件加Content-Encoding: gzip—— 压缩率低且白耗 CPU - 若文件来自 NFS 或 CephFS,需确认挂载选项支持
noatime和direct_io,否则sendfile可能退化为用户态拷贝
最易被忽略的一点:Iris 的 app.Run() 默认不设置 ReadTimeout 和 WriteTimeout,大文件传输过程中客户端断连,连接会一直 hang 住直到操作系统 kill;务必在 iris.Configuration 中显式配置超时,或用 http.Server 手动控制。











