io.copy是go中大文件拷贝的默认且最优选择,内置32kb缓冲区,内存低、性能稳;需确保父目录存在、处理覆盖与错误回滚,一般无需自定义缓冲区。

io.Copy 是大文件拷贝的默认且最优选择
绝大多数场景下,io.Copy 就是 Go 里大文件拷贝的终点——它不花哨、不绕弯,32KB 默认缓冲区在磁盘 I/O 和系统调用开销之间做了成熟权衡。你不需要自己写 Read/Write 循环,也不该为了“极致”去碰底层 syscall。实测中,单线程 io.Copy 在 NVMe 上常跑满 2–3 GB/s,瓶颈早就不在 Go 运行时,而在存储设备本身。
常见翻车点:
-
os.Create不创建父目录,路径不存在会直接报错;必须提前os.MkdirAll(filepath.Dir(dst), 0755) - 目标文件若已存在,
os.Create会截断清空,不是追加;需业务层判断是否覆盖 -
io.Copy不保留权限、atime/mtime;要一致就得额外调用os.Chmod和os.Chtimes - 别用
os.ReadFile+os.WriteFile处理 >100MB 的文件——OOM 风险极高,且无流控
什么时候该换 io.CopyBuffer?
只有当你明确知道 I/O 特性,并且压测验证过收益时,才考虑改缓冲区。比如某些网络文件系统(NFSv4.1、CephFS)在高延迟链路上,64KB 或 128KB 缓冲可能减少 10–15% 的 syscall 次数;但 SSD 本地拷贝中,32KB 仍是更稳的选择。
正确写法是:
buf := make([]byte, 64*1024) _, err := io.CopyBuffer(dst, src, buf)
注意:
- 缓冲区大小不是越大越好:超过 1MB 容易让单次系统调用阻塞太久,掩盖错误
- 不能复用同一块
[]byte给多个并发io.CopyBuffer——没锁保护会数据错乱 -
io.CopyBuffer内部仍走和io.Copy相同逻辑,只是把缓冲区暴露出来;没性能银弹
HTTP 下载大文件别幻想零拷贝
想用 sendfile 或 splice 直接从磁盘送进 socket?HTTP 协议层已经把它拦死了。io.Copy(w, f) 在 HTTP handler 里永远走的是用户态 read+write 路径,strace 下只能看到 read(3, ...) 和 write(4, ...),吞吐卡在 100MB/s 是常态。
唯一能触发内核零拷贝的官方路径是 http.ServeContent,但它有三个硬条件必须同时满足:
- 源必须是
io.ReadSeeker(*os.File符合,但加密包装过的io.Reader不行) - 必须显式设置
w.Header().Set("Content-Disposition", "attachment; filename=...") - 必须传入准确的
modtime(fi.ModTime()),否则缓存逻辑退化,还可能拒绝零拷贝
绕过 http.ResponseWriter 自己调 unix.Splice?得亲手处理管道创建、fd 来源合法性、offset 传递、循环搬运、TLS/HTTP/2 兼容性……实际项目中几乎没人跑通。
并发复制 ≠ 更快,尤其对单块大文件
用 goroutine 并发读同一个 *os.File 文件?会触发内核锁竞争,实测反而比单线程慢 20% 以上。cptX 等工具的并发优势只体现在“大量小文件”场景(如 10 万个 1MB 文件),靠并行 open/write 摊薄 syscall 开销;对单个 50GB 文件,开 8 个 goroutine 去读同一 fd,只是制造更多 seek 和锁等待。
真要压榨单文件吞吐,优先检查这些:
- 挂载选项是否含
noatime(避免每次读都更新 atime) - 目标磁盘是否与源磁盘同属一个 NVMe 控制器(跨控制器反而带宽受限)
- 是否误启了 SELinux/AppArmor 的审计日志(
ausearch -m avc -ts recent可查)
最后提醒一句:所谓“极致”,往往不在代码行数或 syscall 调用次数,而在于你是否清楚每一字节经过了哪几层抽象、谁在真正搬运它。Go 的 io.Copy 已经站在了用户态最合理的边界上。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











