大文件分片上传在gin中不能用c.formfile,因其强制触发parsemultipartform导致请求体被提前读空或解析失败;必须设c.request.parsemultipartform(0),改用io.copyn流式读取application/octet-stream原始body,并通过文件md5生成upload_id、redis记录分片状态、校验crc32及原子化合并确保可靠性。

大文件分片上传在 Gin 中不能靠 c.FormFile 一次性收完——它默认走内存缓存,50MB 文件就可能触发 OOM;必须绕过 Gin 的 multipart 自动解析,直接读 c.Request.Body 流式处理。
分片接收时为什么不能用 c.FormFile
c.FormFile 会强制调用 c.Request.ParseMultipartForm,把整个请求体(含所有分片数据)加载进内存或临时磁盘;一旦分片大小超 router.MaxMultipartMemory(默认 32MB),Gin 就会尝试写临时文件——但分片请求通常不带完整 multipart boundary,导致解析失败,报错类似 http: no such file 或直接 panic。
- 分片上传应使用原始
c.Request.Body,配合io.CopyN或io.LimitReader按需读取指定字节数 - 前端必须用
fetch/XMLHttpRequest手动构造Content-Type: application/octet-stream,而非multipart/form-data——后者会让 Gin 误判为表单,提前解析失败 - 路由要禁用自动 multipart 解析:在 handler 开头加
c.Request.ParseMultipartForm(0)防止后续误触发
upload_id 和分片索引怎么设计才可靠
仅靠前端传的 upload_id + chunk_index 不够——攻击者可伪造索引跳传、重放旧分片。服务端必须绑定文件指纹与上传上下文。
- 推荐用文件内容 MD5(或 SHA-256)做主键,生成
upload_id,避免同名不同文件覆盖 - 每个分片保存路径应为
./uploads/{upload_id}/{chunk_index}.bin,目录权限设为0750防遍历 - Redis 记录用
upload:{upload_id}:parts存已接收索引(SET类型),每次接收前先查是否存在:redis.SIsMember("upload:abc123:parts", "2") - 重复或越界索引直接返回
409 Conflict,不写磁盘、不记日志(防刷库)
合并分片前必须校验的三件事
合并不是简单按序 cat,漏掉任意一项都可能导致损坏文件或安全风险。
- 先查 Redis:
redis.SCard("upload:abc123:parts") == total_chunks,不相等就拒绝合并(前端可能谎报总数) - 再逐个校验每个分片文件存在且大小准确(比如第 0 片应为满 ChunkSize,末片可小于 ChunkSize,但不能为 0)
- 最后用
os.OpenFile(..., os.O_CREATE|os.O_WRONLY|os.O_EXCL)创建目标文件,防止竞态覆盖——若文件已存在,说明有人并发触发合并,应 abort
临时分片文件怎么清理才不丢不漏
分片存储是临时行为,但没人负责删,几天后磁盘就满了。不能依赖定时脚本扫目录,得结合业务生命周期主动清理。
- 成功合并后,立刻
redis.Del("upload:abc123:parts")并os.RemoveAll("./uploads/abc123") - 对超 24 小时未完成的 upload_id,用 Redis key 过期(
EXPIRE upload:abc123:parts 86400)自动淘汰,再配一个低频后台 goroutine 扫描过期 key 对应的本地目录 - 禁止用
defer os.RemoveAll——handler panic 时 defer 不执行;必须在合并逻辑末尾显式调用
真正容易被忽略的是分片传输过程中的校验缺失:不验每个分片的 CRC32 或 MD5,就无法发现网络丢包或中间代理篡改,合并出来的文件可能静默损坏。这个 checksum 必须由前端计算并随分片一起传,服务端收到后立即比对,不匹配就拒收。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











