应优先使用 http.servecontent 而非 io.copy:它支持 range、缓存协商、断连检测,并对 *os.file 自动启用 sendfile 零拷贝;需传入 io.readseeker 且正确设置 content-disposition 与 modtime;超大文件需注意底层是否支持零拷贝,否则应调优缓冲区。

直接用 io.Copy 把文件读进内存再写出去,大文件会瞬间吃光堆内存;正确做法是绕过用户态缓冲、复用底层 I/O 能力,并让 HTTP 协议层自己处理 Range、缓存和断连。
优先用 http.ServeContent 而不是手写流复制
手动 io.Copy(f, w) 看似简单,但会丢掉客户端断连检测、Range 请求支持、If-Modified-Since 缓存协商,还无法利用 sendfile 零拷贝。而 http.ServeContent 内置全部逻辑,且对 os.File 自动走内核零拷贝路径。
- 必须传入实现了
io.ReadSeeker和Stat()的对象(*os.File满足) -
w.Header().Set("Content-Disposition", `attachment; filename="xxx"`)必须显式设置,否则浏览器不触发下载 -
fi.ModTime()必须准确,否则缓存失效,重复传输 - 不要自己包装
os.File或加中间io.Reader,那会强制走用户态拷贝
超大文件(>500MB)需控制缓冲区与 goroutine 占用
即使用了 http.ServeContent,若底层文件系统不支持 sendfile(如某些 NFS、FUSE),Go 仍会 fallback 到用户态读写,此时默认 32KB 缓冲区在高并发下易引发 GC 压力或 P99 延迟抖动。
- 改用
io.CopyBuffer(w, r, make([]byte, 1,缓冲区设为 1MB 更稳 - 切片必须复用,不能每次请求都
make新的 —— 放sync.Pool或全局变量里 - 若文件已
mmap,http.ServeContent会自动识别并跳过用户态拷贝,无需额外操作
反向代理场景:从远程服务流式转发大文件
当你的服务是“中转站”(比如从 S3 或第三方 API 下载后转给前端),不能先落地再发,必须边拉边推。此时 http.ServeContent 不适用,得靠 io.Copy + 头部透传。
- 务必复制关键响应头:
Content-Type、Content-Length(若存在)、Content-Disposition - 禁用
gzip压缩(二进制文件无效,还耗 CPU),但可启用 HTTP/2 让 HPACK 压缩响应头 - 用
httputil.NewSingleHostReverseProxy替代手写转发,它内置连接池、超时控制和头部过滤 - 若需修改响应体(如注入水印),必须用流式解析器(如
xml.Decoder或csv.Reader),避免io.ReadAll
真正卡住性能的往往不是代码行数,而是你是否让操作系统替你干活 —— http.ServeContent 是开关,而 ModTime 是否准确、Content-Disposition 是否设置、缓冲区是否复用,这三个点漏掉任意一个,零拷贝就失效了。











