Golang实现文件拷贝的多种方法对比

io.Copy最省心但需注意三点:不校验源是否为目录、不创建目标父目录、不保留权限和时间戳;应配合os.Stat、os.MkdirAll、os.Chmod、os.Chtimes使用。

直接用 io.Copy 最省心,但要注意缓冲区和错误处理

绝大多数场景下,io.Copy 是首选:它内部自动使用 32KB 缓冲区,兼顾性能与内存占用,且能正确处理流式读写。但容易忽略两点:io.Copy 不校验源文件是否为目录、不创建目标路径父目录、也不保留文件权限和时间戳。

  • 务必先用 os.Stat 检查源文件是否存在且非目录
  • 目标路径的父目录需提前用 os.MkdirAll 创建,否则会报 no such file or directory
  • io.Copy 返回的是实际拷贝字节数和第一个遇到的错误,不能只看 err 是否为 nil —— 要检查返回的 n 是否等于源文件大小(尤其在需要强一致性时)
src, _ := os.Open("a.txt")
dst, _ := os.Create("b.txt")
n, err := io.Copy(dst, src)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("copied %d bytes\n", n)

io.CopyBuffer 手动控制缓冲区大小,适合大文件或内存受限环境

当拷贝单个超大文件(如 >1GB),或运行在嵌入式/容器等内存紧张环境时,io.CopyBuffer 允许你传入自定义缓冲区切片,避免默认 32KB 在高频小文件场景下反复分配。

  • 缓冲区太小(如 4KB)会增加系统调用次数,降低吞吐;太大(如 1MB)可能引发 GC 压力或 OOM
  • 建议按场景选值:普通服务用 make([]byte, 64*1024)(64KB),IoT 设备用 make([]byte, 8*1024)
  • 缓冲区必须是切片([]byte),不能是数组;重用同一缓冲区时注意并发安全
buf := make([]byte, 64*1024)
src, _ := os.Open("large.bin")
dst, _ := os.Create("large_copy.bin")
_, err := io.CopyBuffer(dst, src, buf)
if err != nil {
    log.Fatal(err)
}

完整文件拷贝需组合 os.OpenFile + os.Chmod + os.Chtimes

仅靠 io.Copy 得到的是“内容一致”,但生产环境常要求元数据一致:权限、修改时间、访问时间。Go 标准库不提供原子化“复制+保留属性”的函数,必须手动补全。

  • 权限需用 os.Chmod(dst, info.Mode()),注意 info.Mode() 包含权限位和文件类型位,传给 Chmod 前应 & os.ModePerm
  • 时间戳用 os.Chtimes(dst.Name(), info.ModTime(), info.ModTime())(访问时间通常也设为同值)
  • 符号链接、设备文件等特殊类型需额外判断 info.Mode() & os.ModeSymlink != 0,否则 os.Open 会跟随链接导致误拷内容
src, _ := os.Open("src.go")
info, _ := src.Stat()
dst, _ := os.OpenFile("dst.go", os.O_CREATE|os.O_WRONLY, info.Mode()&os.ModePerm)
io.Copy(dst, src)
dst.Close()
os.Chtimes("dst.go", info.ModTime(), info.ModTime())

第三方库 fsutil.Copycopy.Copy 看似方便,但引入依赖前请确认需求边界

github.com/anatol/copygolang.org/x/exp/fs/copy(实验包)封装了递归、符号链接处理、进度回调等功能,但多数项目并不需要这些。

  • 如果只是单文件、无 symlink、不跨文件系统,标准库已足够,无需额外依赖
  • copy.Copy 对硬链接、ACL、扩展属性等仍不支持,别误以为它是“完全替代”
  • 某些库默认启用递归(如 fsutil.Copy),若传入目录路径却没加判断,可能意外拷贝整个子树

真正复杂的需求(如 rsync 风格同步)不如直接调用 rsync -a 命令,Go 只做 process wrapper —— 更可靠,也省去自己处理 edge case 的成本。