上传中断时 r.body 会提前关闭,需捕获 io.errunexpectedeof;应 defer r.body.close()(先判非 nil),流式读取时显式检查该错误,统一返回 408 或 499,禁用自动 multipart 解析。

上传中断时 r.Body 会提前关闭,必须主动捕获 io.ErrUnexpectedEOF
Go 的 HTTP 服务在客户端断开连接(比如关浏览器、网络闪断)后,r.Body.Read() 会立刻返回 io.ErrUnexpectedEOF 或 net/http: request body closed prematurely。这不是 bug,是底层 TCP 连接中断的正常表现,但很多人误以为是代码逻辑错,直接 panic 或返回 500。
实操要点:
- 在 handler 开头加
defer r.Body.Close(),但必须先判断r.Body != nil,否则调用Close()会 panic - 流式读取时(如用
io.CopyN或io.ReadFull),显式检查 err 是否等于io.ErrUnexpectedEOF或net.ErrClosed - 建议统一返回
http.StatusRequestTimeout(408)或499 Client Closed Request(Nginx 兼容),避免把客户端错误伪装成服务端故障 - 别依赖
http.MaxBytesReader拦截中断——它只防超长请求体,对“读一半断开”完全无效
禁用 r.ParseMultipartForm 自动解析,改用 r.MultipartReader() 流式处理
调用 r.ParseMultipartForm(32(或任何非零值)会强制 Go 把整个 multipart 请求体读完并解析,一旦中断,<code>r.Body 就被消费光,后续无法再读分片数据。这是断点续传失败最常见原因。
正确做法是开头就调用 r.ParseMultipartForm(0) 禁用自动解析,再用 r.MultipartReader() 手动逐 part 处理:
-
r.FormValue("upload_id")或r.URL.Query().Get("chunk_index")获取元信息,别碰r.MultipartForm.Value - 对每个
part,用part.FileName()校验文件名,防止路径遍历(如../../etc/passwd) - 用
io.CopyN(dst, part, chunkSize)写入磁盘,路径按./uploads/{upload_id}/chunk_{index}组织 - 写入前
os.MkdirAll()确保目录存在;并发写同一分片时,打开文件加os.O_EXCL防覆盖
分片上传必须自己维护状态,内存 map 不可靠
Go 标准库不保存上传进度。把 map[string]int64 存在内存里看似简单,但服务重启后全丢,用户重试就只能从头传——这根本不是断点续传。
状态字段至少包含:file_id、uploaded_bytes、total_size、status("uploading"/"completed"),存储必须持久化:
- 单机部署优先选 SQLite(开启 WAL 模式保证原子写入),比纯文件 + flock 更稳
- 分布式场景用 Redis:用
SET upload:{id} "{json}" EX 3600 NX做带过期的原子状态写入 - 每次接收分片前,查状态确认客户端传的
Content-Rangeoffset 是否匹配uploaded_bytes,不匹配就返回409 Conflict - 合并完成前,务必逐片校验哈希(SHA256),别信
Content-Length—— 中间代理可能篡改
os.O_APPEND 是断点续传最大陷阱,写文件必须用 Seek + O_WRONLY
用 os.O_APPEND 打开文件后,所有 Write() 都强制写到末尾,Seek() 完全失效。多线程分片写入时,会导致数据错位、中间填充空字节、文件损坏——这是 POSIX 层面的行为,Go 无法绕过。
正确写法:
- 用
os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0644)打开(不加O_APPEND,也不加O_TRUNC) - 立刻
f.Seek(offset, io.SeekStart),并检查返回值是否等于offset(防止文件被截断) - 推荐用
f.WriteAt(data, offset)替代f.Write(),它不依赖当前光标位置,更安全 - 下载侧同理:先
HEAD获取真实Content-Length,再os.Stat()得本地长度,构造Range: bytes={offset}-,且只在响应码为206时才续传
Seek,而是让服务端状态更新、文件写入、客户端偏移校验三者严格对齐——少一个环节,续传就变成“看起来能用,实际总坏”。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











