http.ServeFile 不适合大文件流式下载,因其会将整个文件读入内存导致OOM,且不支持断点续传、自定义响应头和缓冲区控制;应手动用 io.CopyBuffer 分块传输,配合 Range 解析、Content-Length 设置、写超时及 Flush 控制实现稳定流式下载。

为什么用 http.ServeFile 不适合大文件流式下载
直接调用 http.ServeFile 会把整个文件读进内存再发出去,对几百 MB 以上的文件极易触发 OOM;它也不支持断点续传、自定义响应头(比如 Content-Disposition),且无法控制缓冲区大小和写超时。真正做流式下载,得绕过这个封装,手动操作 ResponseWriter 和 io.Reader。
如何用 io.CopyBuffer 实现可控的流式传输
核心是避免一次性加载文件,而是分块读写。用 os.Open 打开文件后,直接传给 io.CopyBuffer,并显式指定缓冲区大小(如 32KB):
file, _ := os.Open(filepath)
defer file.Close()
// 设置响应头
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)
// 流式写入,避免内存暴涨
buf := make([]byte, 32*1024)
_, err := io.CopyBuffer(w, file, buf)
if err != nil && !errors.Is(err, syscall.EPIPE) {
log.Printf("copy failed: %v", err)
}
-
io.CopyBuffer比io.Copy更可控——默认缓冲区是 32KB,太小会导致 syscall 过多,太大又浪费内存 - 必须检查
syscall.EPIPE错误:客户端中断连接(比如关掉浏览器下载页)时会触发,这不是服务端 bug,不用报错日志 - 别忘了设
Content-Length:如果知道文件大小,提前写上能启用浏览器进度条;否则浏览器显示“未知大小”,但下载仍可进行
怎么支持 Range 请求实现断点续传
客户端带 Range: bytes=1000-2000 头时,需解析并返回 206 Partial Content。Go 标准库没自动处理,得自己解析 r.Header.Get("Range"):
rangeHeader := r.Header.Get("Range")
if rangeHeader != "" {
start, end, ok := parseRange(rangeHeader, fileInfo.Size())
if !ok {
http.Error(w, "Invalid Range", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusPartialContent)
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileInfo.Size()))
w.Header().Set("Content-Length", strconv.FormatInt(end-start+1, 10))
http.ServeContent(w, r, filename, fileInfo.ModTime(), §ionReader{file, start, end})
return
}
-
parseRange需处理三种格式:bytes=100-、bytes=-100、bytes=100-200,注意边界校验(不能超过文件大小) - 别直接用
http.ServeContent包裹整个文件——它内部会尝试读取全部内容判断 ETag,对大文件不友好;应传入一个只读指定区间的io.ReadSeeker(比如自定义sectionReader) - 响应必须带
Content-Range和正确Content-Length,否则 Chrome/Firefox 不会恢复断点
为什么必须设置 Flush 和超时控制
流式下载过程中,TCP 缓冲区可能积压数据,导致客户端长时间无响应;同时,慢速客户端或网络抖动会让连接挂住,拖垮 goroutine 资源:
立即学习“go语言免费学习笔记(深入)”;
- 在循环写入或
io.CopyBuffer前,调用w.(http.Flusher).Flush()强制刷出 HTTP header,让客户端立刻知道下载开始 - 用
http.TimeoutHandler包裹 handler 是不够的——它只限制 handler 执行时间,不控制流式写入过程;应在 handler 内部对ResponseWriter设置写超时:w.(http.Hijacker).Hijack()太重,推荐用context.WithTimeout控制整个请求生命周期 - 若用
net/http.Server,务必设置WriteTimeout和ReadTimeout,否则慢连接会无限占用连接数
流式下载真正的复杂点不在“怎么发”,而在“怎么稳发”:缓冲区大小、错误容忍、连接保活、客户端兼容性,每项都得实测调整,光靠文档参数容易翻车。


















