正确做法是用http.servecontent启用零拷贝传输,调用sendfile或transmitfile系统调用跳过用户态内存拷贝,需满足:使用io.readseeker+stat()对象、设置准确modtime、显式指定content-disposition头。

直接用 io.Copy 把文件读进内存再写出去,大文件(比如 500MB+)会瞬间吃光应用内存,甚至触发 OOM kill。正确做法是绕过内存缓冲,让内核直接搬运数据。
用 http.ServeContent 启用零拷贝传输
它不是“把文件读出来再发”,而是调用 sendfile(Linux)或 TransmitFile(Windows)系统调用,跳过用户态内存拷贝。但必须满足三个条件:
-
http.File或实现了io.ReadSeeker+Stat()的对象 —— 普通*os.File就符合 - 设置准确的修改时间:
fi.ModTime(),否则If-Modified-Since缓存失效 - 显式设置
Content-Disposition头,否则浏览器可能尝试渲染而非下载
错误写法:io.Copy(w, f) 会忽略 Range、断连重试、ETag 等,客户端断网重连就得重下整份。
大文件代理转发时避免中间缓冲
从第三方 URL 下载并实时转给客户端(如 CDN 回源),不能先存本地磁盘或内存,得流式透传:
- 用
http.DefaultClient发起请求,检查res.StatusCode和res.Header.Get("Content-Type") - 把关键响应头(
Content-Type、Content-Length、Content-Disposition)手动复制到w.Header() - 直接
io.Copy(w, res.Body)—— 此时w是http.ResponseWriter,res.Body是io.ReadCloser,天然流式 - 务必
defer res.Body.Close(),否则连接不释放,后端服务可能被压垮
注意:如果上游返回了 Transfer-Encoding: chunked,Content-Length 为空,此时浏览器无法显示进度条,但不影响功能。
缓冲区大小与复用策略
默认 io.Copy 用 32KB 缓冲区,对 GB 级文件,频繁分配/释放小切片会增加 GC 压力:
- 改用
io.CopyBuffer(w, r, make([]byte, 1(1MB 缓冲),平衡吞吐与内存驻留 - 不要在 handler 内每次
make,可定义全局变量:var bufPool = sync.Pool{New: func() interface{} { return make([]byte, 1 - 若文件已用
mmap打开,http.ServeContent会自动识别并跳过用户态拷贝,比手动io.CopyBuffer更优
并发高时,缓冲区太大(如 4MB)反而导致 goroutine 长时间占用 M,建议 256KB~1MB 区间实测 P99 延迟。
HTTP/2 与反向代理陷阱
HTTP/2 的多路复用能显著降低高并发下的连接开销,但容易被反向代理悄悄降级:
- 服务端必须启用 TLS(Go 中 HTTP/2 强制要求),且证书有效 —— 否则
http.Server会静默回退到 HTTP/1.1 - Nginx 等反代需显式开启
http2并配置proxy_http_version 1.1(不是 2.0!),否则 Alt-Svc 头不生效 - 用
curl -I --http2 https://yoursite.com/file.zip验证,响应头中出现alt-svc才算真正启用了 HTTP/2
最易被忽略的是:即使代码写了 http.ServeContent,只要反代没配好,内核零拷贝能力就废了一半 —— 数据先到反代内存,再转发,又变回用户态拷贝。











