io.copy是字节流搬运函数,非文件复制专用函数;它只接受io.reader和io.writer接口值(如*os.file),不处理文件打开、权限、时间戳、目录创建或落盘保证,需手动管理资源与元数据。

io.Copy 不是“复制文件”的函数,它是“在两个流之间搬运字节”的函数。它不打开文件、不创建目录、不继承权限、不保证落盘、也不关闭任何东西——你传给它的必须已经是能 Read 和 Write 的东西。
为什么 io.Copy("a.txt", "b.txt") 编译不过
因为 io.Copy 的签名是 func Copy(dst Writer, src Reader) (int64, error),它只接受实现了 io.Reader 和 io.Writer 接口的值,比如 *os.File、net.Conn、bytes.Buffer,而不是字符串路径。
- 传
"a.txt"会报错:cannot use "a.txt" (type string) as type io.Reader - 新手常写的
io.Copy(os.Create("dst"), os.Open("src"))更危险:两个os.Open/os.Create可能失败并返回nil,导致io.Copypanic - 正确顺序永远是:先打开、检查错误、再传入
io.Copy
复制后文件内容对但属性全丢,怎么办
io.Copy 只管字节流,不管元数据。复制完的文件默认是 0644 权限、修改时间是当前时间、不保留符号链接、不继承 ACL 或 birthtime。
- 要保留源文件权限:用
os.Stat拿到srcInfo.Mode(),复制后再调os.Chmod(dst, mode) - 要保留修改时间:调
os.Chtimes(dst.Name(), srcInfo.ModTime(), srcInfo.ModTime()) - 要复制符号链接本身(而非目标内容):不能用
os.Open,得用os.Readlink+os.Symlink - 想省事且 Go ≥ 1.19:直接用
os.CopyFile(src, dst, 0),它自动处理权限、原子性、重试和跨文件系统 fallback
复制大文件时卡住、不完整或磁盘没写满
现象不是 io.Copy 本身的问题,而是资源管理或系统行为没兜住:
-
dst.Close()必须显式调用,否则内核缓冲区可能未刷盘,stat看大小对,但cat读不到最后几 KB - 目标路径父目录不存在?
os.Create不会递归建目录,得提前os.MkdirAll(filepath.Dir(dst), 0755) - NFS 或慢存储场景下,默认 32KB 缓冲可能吞吐不足:换
io.CopyBuffer(dst, src, make([]byte, 1(64KB)试试 - 复制中途出错(如磁盘满),已写入部分不会回滚——业务层需决定是否
os.Remove(dst)清理脏数据
如何加进度、超时或中断支持
io.Copy 本身不提供这些能力,必须靠包装:
- 加进度:写一个带回调的
io.Reader,在Read(p []byte)里累加并触发回调;或用io.TeeReader(src, hashWriter)同步算校验和 - 加超时:用
context.WithTimeout包装src或dst,例如&ctxReader{ctx: ctx, r: src},在Read中 selectctx.Done() - 加中断:同上,用
ctx.CancelFunc触发;不要在defer里调io.Copy,错误上下文会丢失 - 别自己循环
Read/Write:除非真有 per-chunk 处理逻辑,否则性能不如io.Copy内置的缓冲循环
最常被跳过的其实是 Close —— 它不只防 fd 泄漏,还关系到数据是否真正落盘、错误是否被最终暴露。写完 io.Copy 后,别忘了 dst.Close() 和 src.Close(),且顺序别反(尤其网络流)。事情说清了就结束。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











