必须在任何读取请求体操作前调用 r.parsemultipartform(0),因为 go 的 http.request 默认不自动解析 multipart,仅当首次访问 r.multipartform 时才隐式调用 r.parsemultipartform(32mb),若此前已读取 body(如中间件消费),则解析失败并 panic。

Go 标准库不提供断点续传能力,服务端必须自己解析 Content-Range 或 Upload-Offset,原子更新状态与文件写入,否则客户端重试就等于重传——这不是续传,是重复上传。
为什么 r.ParseMultipartForm(0) 必须放在 handler 开头第一行
Go 的 http.Request 默认会在首次访问 r.MultipartForm 时自动调用 r.ParseMultipartForm(32,把整个请求体读进内存或临时文件。一旦触发,<code>r.Body 就被消费完,后续 io.CopyN(r.Body, ...) 会读到空数据,分片直接写失败。
- 必须在 handler 入口立即执行
r.ParseMultipartForm(0),传 0 表示禁用自动解析 - 元信息(如
file_id、chunk_index)只能从 URL 查询参数或r.FormValue()获取,不能依赖r.MultipartForm.Value - 若仍需表单字段(比如 JSON 描述),改用
r.MultipartReader()流式解析,避免全量加载
os.OpenFile 怎么开才不会写错位置
os.O_APPEND 是断点续传最大陷阱:它会让所有 Write() 忽略 Seek(),强制写到文件末尾。哪怕你 f.Seek(1024*1024, io.SeekStart) 成功了,f.Write(data) 还是落到末尾,中间全是空白填充。
- 正确打开方式:
os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644)(不加O_APPEND,也不加O_TRUNC) - 打开后立刻调用
f.Seek(offset, io.SeekStart),并检查返回值是否等于offset;不等说明文件已被截断或外部修改 - 更安全的写法是
f.WriteAt(data, offset),它不依赖当前光标位置 - 写入前可选
f.Truncate(expectedSize)防止稀疏文件问题
怎么校验客户端传来的 offset 真实有效
只靠客户端说“我从 10MB 开始”不可信。网络错误、重试乱序、恶意请求都可能导致偏移错位。服务端必须主动验证一致性。
- 解析
req.Header.Get("Content-Range"),用net/http/httpguts.ParseContentRange(Go 1.22+)或手写 parser,别用正则硬拆 - 校验
start是否等于当前文件长度(fi.Size()),不等就返回409 Conflict - 范围越界(如
bytes 1024-2047/1024)直接返回416 Range Not Satisfiable - 生产环境建议搭配哈希校验:客户端传
X-Prev-Chunk-Hash,服务端读取对应区间比对,失败则返回正确Content-Range
状态存在哪?为什么不能用 map[string]int64
状态存内存(sync.Map 或普通 map)重启就丢;存文件没锁会导致多协程写冲突;存 Redis 或 SQLite 才算真正可用。
- 至少存 5 个字段:
file_id、uploaded_bytes、total_size、last_modified、status - 推荐组合:
Redis存状态(支持过期 + 原子操作),SQLite WAL模式存结构化记录,本地文件系统存分片 - 并发写同一
file_id时,用Redis SET key value EX 30 NX加分布式锁,粒度控制在单个上传 ID 而非全局 - 合并完成前,临时文件必须保留;合并成功后,用
os.Rename()原子替换目标文件,再清理临时状态
最易被忽略的是:状态更新和文件写入必须在同一原子上下文里。哪怕用了 Redis 锁,如果状态写入 DB 和 WriteAt 不在同一个 goroutine 或事务中,就可能出现“状态说写了 5MB,但文件只有 3MB”的静默不一致。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











