必须用io.ReadAt按偏移读取分片,禁用bufio.Reader包裹,每次截断buf[:n]避免脏数据,复用单文件句柄,并校验err==io.EOF或I/O错误。

直接用 os.ReadFile 或 io.ReadAll 读大文件上传,内存必爆——1GB 文件就吃掉 1GB RAM,这不是 bug,是设计使然。必须用流式切片 + 显式限流 + 磁盘暂存,三者缺一不可。
怎么用 io.ReadAt 安全读取指定分片
io.ReadAt 是唯一能安全并发读同一文件不同 offset 的方式,bufio.Reader 包裹后调 ReadAt 会失效,且破坏偏移逻辑。
- 每次读前显式计算
offset := int64(chunkIndex) * chunkSize,传给f.ReadAt(buf[:], offset) - 返回的
n可能小于len(buf)(尤其最后一块),必须用buf[:n]截断再上传,否则带脏数据 - 错误只接受
err == io.EOF或具体 I/O 错误;err == nil && n == 0是非法空块,应跳过或报错 - 别在循环里反复
os.Open,开一次文件句柄,所有分片复用它
为什么不能用 r.ParseMultipartForm(0) 或默认 multipart 解析
Go 的 http.Request 默认首次访问 r.MultipartForm 就自动调 r.ParseMultipartForm(32<span>MB),把整个请求体读进内存或临时文件,破坏流式写入能力,导致分片错位、丢失或 500 崩溃。
- 必须在 handler 开头立刻调
r.ParseMultipartForm(0)—— 注意:这等价于禁用 multipart 解析,不是“不限制”,而是彻底绕过 - 元数据(如
file_id、chunk_index)必须从 URL 查询参数或表单字段显式获取,不能依赖r.MultipartForm.Value(可能为空或未解析) - 二进制数据直接从
r.Body读,用io.CopyN(dst, r.Body, chunkSize)写磁盘,路径建议为./uploads/{file_id}/chunk_{index:04d} - 写入前用
os.MkdirAll确保目录存在;并发写同一分片时,用os.O_CREATE | os.O_WRONLY | os.O_EXCL打开,防止覆盖
并发上传时如何避免连接耗尽和错片
开 100 个 goroutine 同时发 http.Post,不出三秒就触发 too many open files 或服务端 429,根本不是网络慢的问题,是资源失控。
立即学习“go语言免费学习笔记(深入)”;
- 真正要控的是“同一时刻最多几个 HTTP 请求”,用
make(chan struct{}, 3)实现轻量信号量,每个请求前sem <- struct{}{},结束后<-sem -
http.Client.Transport.MaxIdleConnsPerHost必须 ≥ 并发数(如设为 10),否则连接复用失败,反而加重开销 - 每个分片上传前,用
file.Seek(offset, io.SeekStart)定位,再构造io.LimitReader(file, size)作为 body,避免指针混乱 - 重试必须幂等:HTTP Header 加
X-Part-Number和X-Part-SHA256,服务端先查该分片是否已存在,仅对缺失的发起请求
合并分片时为什么不能用 os.File.WriteAt 或字符串排序
用 WriteAt 并发写不同 offset 表面可行,实际极易因分片大小不均、goroutine 调度错乱导致 offset 偏移,文件直接损坏;按文件名字符串排序(如 chunk_1、chunk_10)则会把 chunk_10 排在 chunk_2 前面,顺序全乱。
- 合并必须严格按
chunk_index升序打开分片文件,用io.MultiReader拼接后一次性io.Copy到目标文件 - 先检查是否存在全部
0..N-1分片,缺任一片就返回400 Bad Request,不合并 - 合并完成后立刻用
sha256.New()+io.Copy计算整体哈希,与客户端提交的X-Final-SHA256对比,不匹配则删掉最终文件和所有分片 - 分片路径命名必须带零填充(如
chunk_0001),避免文件系统 readdir 顺序不可靠
最易被忽略的一点:服务端状态必须与客户端预期严格比对,而不是相信“我发了 100 个请求,就该有 100 个成功”。/upload/status?file_id=xxx 返回的已传列表,才是唯一可信依据。


















