真正优雅的流式传输是让数据不驻留、不堆积、不越界,需显式控制读写节奏与边界以应对GB级压力;上传须绕开ParseMultipartForm内存陷阱,下载须支持Range头与Content-Range响应,io.Copy非万能,缓冲区需按存储介质调优。

别用 os.ReadFile,也别信“io.Copy 万能”——它默认 32KB 缓冲在 NFS 或云盘上吞吐暴跌;真正优雅的流式传输,是让数据不驻留、不堆积、不越界,靠显式控制读写节奏和边界来扛住 GB 级压力。
上传时必须绕开 ParseMultipartForm 的内存陷阱
HTTP handler 一进来就调 r.ParseMultipartForm(0)(传 0 表示跳过缓存),否则 r.Body 被提前消费,后续无法流式读取分片。这不是配置问题,是行为级锁死。
- 元信息(
uploadId、chunkIndex)统一从 URL 查询参数或r.FormValue()获取,别碰r.MultipartForm - 二进制分片数据直接从
r.Body流读,用io.LimitReader(r.Body, chunkSize)精确截断,避免越界 - 写入临时分片时用
os.O_CREATE | os.O_WRONLY | os.O_EXCL,防止并发写同一序号导致覆盖
下载必须支持 Range 头与 Content-Range 响应
否则前端视频拖拽、断点续传全失效。标准库 http.ServeContent 可用,但前提是传入的 io.ReadSeeker 要能 Stat() 并正确响应 Seek(0, io.SeekStart)。
- 打开文件用
os.OpenFile(path, os.O_RDONLY, 0),避免因锁或权限导致Seek失败 - 读取前先
stat获取总大小,校验Range合法性(如bytes=0-1023不能超文件长度) - 响应头必须含
Content-Range、Accept-Ranges: bytes、Content-Length,缺一不可
io.Copy 不是银弹:缓冲区大小决定吞吐上限
默认 32KB 在高延迟盘(NFS/Ceph/USB)上 syscall 过多,实测设成 make([]byte, 1024*1024)(1MB)可提速 3–5 倍。但别用 sync.Pool 复用这个缓冲——压测反而更慢。
立即学习“go语言免费学习笔记(深入)”;
- 对 HTTP 响应体转发,若上游不可控(如代理),加
time.AfterFunc监控单次Write超时,主动断连 - 写慢盘时,
io.CopyBuffer(w, r, buf)比裸io.Copy更可控 - 千万别用
bytes.Buffer.ReadFrom(r)或已弃用的ioutil.ReadAll——效果等同于os.ReadFile
gRPC 流式传大文件,stream 关键字位置错一个字就废
proto 中 stream 写错位置,生成的 Go 接口就完全不能用。比如客户端流必须是 rpc Upload(stream Chunk) returns (UploadResult),写成 rpc Upload(Chunk) returns (stream UploadResult) 就变成服务端流,调 Recv() 永远等不到数据。
- 客户端分块发送必须每次构造新
*pb.Chunk实例,别复用 proto message - 服务端接收不能用
for range stream.Recv(),得用带context.WithTimeout的stream.RecvMsg(&msg) - 每个
Chunk必须带offset和单调递增seq,服务端落盘前校验checksum,失败立即返回status.Error(codes.Aborted, "checksum mismatch")
最易被忽略的点:分片合并时,chunk_10 必须排在 chunk_2 后面——这是数值排序,不是字符串排序;且哈希校验必须逐片重算,不能只比对客户端传来的 X-Chunk-Hash 值。漏掉任一环节,文件就 silently 损坏。


















