io.Copy 看起来只是一个读写循环,实际上先看两端有没有实现 WriterTo / ReaderFrom。这篇用计数包装、分配统计和真实文件/TCP 拷贝验证它怎么选实现、缓冲区多大,以及隐藏接口会付出多少代价。
两边都只暴露 Read / Write 时,io.Copy 把 1048576 字节(1 MiB)拷过去:调用 Read 33 次,前 3 次传入的缓冲区长度 = [32768 32768 32768] 目标换成 io.Discard(它自带 ReadFrom):Read 129 次,缓冲区长度 = [8192 8192 8192] io.CopyBuffer 传 4096 字节的缓冲区:Read 257 次,缓冲区长度 = [4096 4096 4096] 源是 io.LimitReader:限 100 字节时每次 io.Copy 分配 200 B;限 1 MiB 时每次分配 32856 B(源码里会按 LimitedReader.N 缩小缓冲区)
两边都只有 Read/Write 时,io.Copy 用 32 KiB 缓冲区,1 MiB 要 33 次 Read;如果目标是 io.Discard,它自带 ReadFrom,变成 8 KiB 缓冲区,129 次。源是 io.LimitReader 时缓冲区会缩小到限制值,限 100 字节时每次 io.Copy 只分配 200 B。
源实现了 WriterTo:io.Copy 调用了 WriteTo 1 次,Read 0 次(WriteTo 里自己再 Copy 时会走 Read) 目标实现了 ReaderFrom、源只有 Read:ReadFrom 1 次,Write 0 次 两边都只暴露 Read / Write:ReadFrom 0 次,Write 32 次 拷到 io.Discard:源是 *bytes.Reader(有 WriteTo)每次 1 次分配;把它包成只有 Read 的 struct 后每次 2 次分配
源有 WriterTo 就用 WriteTo;否则目标有 ReaderFrom 就用 ReadFrom;都没有才自己循环。给 *bytes.Reader 套一层 struct{io.Reader} 就把 WriteTo 藏起来了,分配也从 1 次变成 2 次。
文件到文件 256 MiB:io.Copy(*os.File, *os.File) 中位 83 ms(3103 MiB/s);包一层隐藏 ReaderFrom/WriterTo 后 79 ms(3261 MiB/s) 读 256 MiB 文件算 SHA-256:io.Copy(hash, file) 中位 127 ms;io.Copy(Discard, io.TeeReader(file, hash)) 中位 144 ms 文件到本机 TCP 连接 256 MiB:io.Copy(*net.TCPConn, *os.File) 中位 57 ms(4506 MiB/s);隐藏接口后 109 ms(2357 MiB/s)
文件到本机 TCP,走 *net.TCPConn 的 ReadFrom 只要 57 ms(4506 MiB/s),包一层隐藏后 109 ms。文件到文件在这台 macOS 上两种写法都是约 80 ms,没有差别。第一次测的时候我没有交替运行,得到过 254 对 177 ms 的“差距”,那是顺序和缓存的影响;交替运行后消失了。TeeReader 多包一层来算 SHA-256,耗时多了约 13%。
Copy copies from src to dst until either EOF is reached on src or an error occurs. It returns the number of bytes copied and the first error encountered while copying, if any. A successful Copy returns err == nil, not err == EOF. Because Copy is defined to read from src until EOF, it does not treat an EOF from Read as an error to be reported. If src implements WriterTo, the copy is implemented by calling src.WriteTo(dst). Otherwise, if dst implements ReaderFrom, the copy is implemented by calling dst.ReadFrom(src).
文档写明:src 有 WriterTo 就调用它,否则 dst 有 ReaderFrom 就调用它;成功时返回 err == nil 而不是 EOF。写自己的类型时,包装 reader/writer 要想想是否因此失去了这些快速路径。