os.readfile + http.post 上传大文件会卡死或 oom,因其将整个文件一次性加载进内存,再经 bytes.buffer 拼接请求体,导致多份内存拷贝;正确做法是用 *os.file + multipart.writer + io.copy 实现流式零拷贝上传,避免 go heap 中转。

为什么 os.ReadFile + http.Post 上传大文件会卡死或 OOM
因为 os.ReadFile 会把整个文件一次性加载进内存,再交给 http.Post 构造请求体——几百 MB 的文件直接触发 GC 压力或直接 panic。HTTP 客户端底层仍用 bytes.Buffer 拼接 body,没做流式缓冲。
- 真实错误现象:上传 500MB 文件时进程 RSS 内存飙升到 1.2GB+,然后被系统 OOM killer 杀掉
- 根本原因不是网络慢,而是内存中同时存着原始文件字节、multipart boundary、base64 编码(如果用了)、HTTP header 等多份拷贝
- 正确路径是绕过内存拼接,让数据从磁盘 → OS socket buffer → 网络,中间不落地到 Go heap
用 io.Copy 实现零拷贝上传流
核心是让 http.Request.Body 成为一个可流式读取的 io.Reader,且不依赖内存缓冲。最直接的方式是把打开的 *os.File 直接塞进去,配合 multipart.Writer 构建 boundary 流。
- 不要调用
r.ParseMultipartForm()—— 它会把整个 multipart body 读进内存 - 服务端接收时,用
multipart.NewReader(r.Body, boundary)边读边解析,不缓存整块 - 客户端构造时,用
mpw, _ := multipart.NewWriter(w),然后mpw.CreateFormFile("file", name)返回一个io.Writer,再用io.Copy(partWriter, file)把文件内容直接写入 multipart 流 - 关键点:整个过程没有
[]byte中转,OS page cache 和 socket send buffer 承担了缓冲职责
Transport 层必须复用连接并调大 IdleConnTimeout
默认 http.DefaultTransport 对大文件上传极其不友好:每个请求新建 TCP 连接,TLS 握手耗时长,且连接很快被回收,无法利用 TCP fast open 或 TLS session resumption。
- 实测对比:单次上传 1GB 文件,复用连接比每次新建快 2.3 倍(主要省在握手和 slow start)
- 必须设置:
MaxIdleConns≥ 并发数,MaxIdleConnsPerHost≥ 并发数,IdleConnTimeout至少 90s(短于服务端 keepalive timeout) - 若服务端支持 HTTP/2,启用后自动多路复用,单连接并发上传多个文件也不冲突
- 注意:别漏掉
TLSClientConfig.InsecureSkipVerify = false(生产环境必须校验证书)
分块上传时如何避免 goroutine 泄漏和句柄耗尽
分块上传看似能控内存,但若没管好资源生命周期,很容易在高并发下崩掉——不是 CPU 或带宽瓶颈,而是 openat(2) 系统调用失败或 too many open files。
- 每个分块 goroutine 必须显式
defer file.Close(),且不能把*os.File传给闭包后丢弃引用(否则文件句柄不释放) - 用带缓冲 channel 当信号量,比如
sem := make(chan struct{}, 5)控制最大并发分块数,机械硬盘上 3–5 个足够,SSD 可放宽到 10–15 - 上传失败的分块要主动清理临时文件,否则磁盘空间悄悄吃满;建议用
os.Remove+os.RemoveAll清理失败目录 - 别在循环里无限制
go uploadChunk(...),必须等sem 成功后再启 goroutine
实际跑通的关键细节往往藏在 Transport 配置和文件句柄管理里,而不是算法或协议设计本身。多数人卡在“上传变慢”,真正问题却是 net.Conn 不复用,或 os.Open 后忘了 Close。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











