io.copy 会丢碎片是因为其不保证原子写入,且 os.file.write 可能只写入部分字节,又不重试或校验;“碎片丢失”实为 seek 和 write 未加锁导致竞态,日志无报错但文件内容缺失。

Go 文件上传时为啥 io.Copy 会丢碎片?
因为 io.Copy 默认不保证原子写入,且底层 os.File.Write 可能只写入部分字节(尤其在磁盘满、网络挂载或信号中断时),而它又不主动重试或校验。你看到的“碎片丢失”,往往是写入长度
- 务必检查
io.Copy返回的n和err:即使err == nil,n也可能小于源长度(比如读到 EOF 前就断了) - 生产环境别直接用
io.Copy(dst, r)接 HTTP body;改用io.CopyN或分块 + 显式长度校验 - HTTP 上传若带
Content-Length,先比对r.ContentLength和实际读取字节数,不一致就拒收
Go 断点续传必须自己管 offset 和校验
HTTP 协议本身不定义文件上传断点续传标准(不像下载有 Range),所以服务端得靠客户端约定字段(如 X-Upload-Offset、X-Upload-ID)来恢复。Go 标准库不提供现成支持,全得手写逻辑。
- 上传前客户端需生成唯一
upload_id,每次请求带上当前已写 offset(如X-Upload-Offset: 10240) - 服务端收到后:用
os.OpenFile(path, os.O_WRONLY|os.O_APPEND, 0)打开文件,再用f.Seek(offset, io.SeekStart)定位 —— 注意不是os.O_APPEND自动跳,它只对Write生效,Seek后仍可覆写 - 写完立刻调用
f.Sync(),否则 OS 缓存可能让断电后最后几 KB 永久丢失 - 强烈建议每块写入后计算 SHA256 片段哈希,和客户端传来的
X-Chunk-Hash对比,防网络篡改或粘包错位
multipart/form-data 解析时容易漏 chunk 边界
浏览器发的 multipart 请求,每个文件块是独立 part,但 Go 的 r.MultipartReader() 或 r.ParseMultipartForm() 默认把整个 body 读进内存或临时文件,一旦中断,整个 part 就丢了 —— 你根本没机会按 chunk 处理。
- 别用
r.FormFile:它隐式调用ParseMultipartForm,无法流式处理 - 改用
mime/multipart.Reader手动解析:mr, err := r.MultipartReader(),然后循环mr.NextPart(),对每个part流式读、校验、写盘 - 注意
part.Header.Get("Content-Disposition")里的filename可能为空(比如 base64 分片),别硬解 - 如果客户端分片命名不规范(如
file_001,file_002),服务端得自己拼接顺序,别依赖 multipart 的 part 出现顺序 —— HTTP/2 可能乱序
并发上传多个 chunk 时 os.File 的竞态很隐蔽
多个 goroutine 对同一个 *os.File 并发 Write 或 Seek+Write,不会 panic,但数据会错位覆盖,而且无明确错误提示 —— 因为底层 fd 是共享的,POSIX write() 调用本身是原子的,但 Seek 和 Write 是两步,中间可能被其他 goroutine 插入。
立即学习“go语言免费学习笔记(深入)”;
- 绝对不要让多个 goroutine 共享一个
*os.File写同一文件 - 方案一:用单个 goroutine 串行写,其他 goroutine 把 chunk 发到 channel,由 writer goroutine 统一 seek/write/sync
- 方案二:每个 chunk 写独立临时文件(
file_001.part),上传完成后再用cat file_*.part > final.bin拼接(Linux)或io.Copy顺序合并(跨平台) - 方案三:用
sync.Mutex包裹Seek+Write+Sync全流程,但会严重降低吞吐,仅适合小流量场景
最麻烦的不是写错,而是错得不报错 —— 日志里一切正常,文件打开却缺开头 2KB,这种问题查三天都想不到是 Seek 和 Write 没锁住。










