直接用 io.copy 上传大文件会失败,因跨云对象存储有单次上传限制(如 s3 5gb)、易超时、内存暴涨且无断点续传;需统一抽象分片上传流程,通过高阶函数封装切片、并发上传、合并三步,适配各云厂商差异。

为什么直接用 io.Copy 上传大文件会失败
跨云对象存储(如 AWS S3、阿里云 OSS、腾讯云 COS)对单次上传有明确限制:S3 是 5GB,OSS 是 5TB 但推荐分片上传,COS 要求大于 100MB 必须用分片。直接用 io.Copy 传一个 10GB 文件,不仅超时、内存暴涨,还会因网络中断导致整个上传失败,重试成本极高。
分片上传本质是把文件切块、并发上传、最后合并。但各云厂商 SDK 接口差异大:PutObject vs PutObjectRequest vs InitiateMultipartUpload,参数名、回调逻辑、错误码都不统一。硬写三套逻辑维护成本高,且并发控制、断点续传、内存复用等细节极易出错。
- 别在主 goroutine 里循环调
UploadPart——没并发,还卡死 - 别用
bytes.Buffer加载整块分片到内存——100MB × 并发数 = OOM 风险 - 别忽略
Content-MD5或ETag校验——云厂商返回的 ETag 不一定是 MD5(如 S3 对非 multipart 是 MD5,multipart 是拼接后 hex(md5(每个 part))),校验错会导致合并失败
如何用高阶函数抽象出统一的分片上传流程
核心不是封装 SDK,而是把「切片 → 并发上传 → 合并」三步中可变的部分抽成函数参数。比如:initUpload 返回 upload ID 和初始化上下文;uploadPart 接收 part number、数据 reader、长度,返回 part ETag;completeUpload 接收所有 part ETag 列表完成合并。
这样,不同云厂商只需实现这三个函数,其余调度、重试、限流、进度回调全复用。例如阿里云 OSS 的 uploadPart 实际调的是 bucket.PutObject with oss.PartNumber 和 oss.UploadID;而 S3 则调 s3manager.Uploader.UploadPart。
- 高阶函数签名建议为:
func(uploadID string, partNum int, r io.Reader, size int64) (string, error),返回 ETag(必须字符串,因 COS 返回的是 base64 编码的 MD5) - 切片逻辑必须支持 offset + length,不能依赖
os.File.ReadAt——某些云 SDK 内部会 seek,而对象存储的 reader 可能不支持 - 并发数别硬编码,用
sem := make(chan struct{}, concurrency)控制 goroutine 数量,避免打爆云服务端连接池
分片大小设成 5MB 还是 10MB?关键看云厂商最小分片限制
不是越大越好。S3 最小分片 5MB(除最后一片),OSS 是 100KB,COS 是 1MB。设成 3MB 会导致 S3 UploadPart 直接报错 EntityTooSmall;设成 100MB 则可能触发 COS 单请求超时(默认 60s)。
实操建议:取各云厂商最大最小值交集,选 8MB。既能避开 S3 下限,又留出网络波动缓冲空间。更重要的是——分片大小必须全程一致,否则 CompleteMultipartUpload 会拒绝合并(S3 明确要求除最后一片外 size 必须相同)。
- 用
math.MaxInt64计算最后一片大小,别用file.Size() % partSize——负数取模在 Go 里行为和 Python 不同 - 分片序号从 1 开始(不是 0),S3/OSS/COS 全部要求如此,错填
PartNumber=0会返回InvalidArgument - 上传前先
stat获取文件 size,避免边读边发现 EOF 导致最后一片 size 错误
断点续传怎么判断哪些分片已成功?靠 ETag 还是 ListParts?
别依赖本地记录或时间戳。唯一可靠方式是调用各云的 ListParts 接口,传入 upload ID,拿到已上传的 part number 和 ETag 列表。S3 返回 CompletedPart 结构,OSS 返回 UploadedPart,COS 返回 CompletePart ——字段名不同,但都含 PartNumber 和 ETag。
注意:ListParts 本身也有分页限制(S3 默认 1000 条),大文件分片多时要循环调用。且该接口不保证强一致性,刚上传完立刻查可能查不到,需加短重试(如 3 次,间隔 100ms)。
- 比较 ETag 时用
strings.Trim(etag, `"`) == expected——云返回的 ETag 带双引号,不 trim 会导致校验失败 - 不要跳过已上传分片再重新上传——哪怕内容完全一样,ETag 也可能不同(如 COS 对同一内容多次上传会生成不同 ETag)
- 合并失败后,必须显式调
AbortMultipartUpload,否则 upload ID 会一直占用,OSS 7 天后自动清理,但 S3 不清理,浪费资源
uploadPart 调用里包一层 ctx, cancel := context.WithTimeout(parentCtx, 30*time.Second),且 cancel 必须 defer 调用。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











