io.copy

io.Copy是一个非常好用的函数,能够很方便地对数据进行拷贝。本文将会从定义、用法、底层源码逐一来讲解。并在文末通过项目见闻,来加深大家的io.Copy的理解与思考。

io.Copy

基本定义

io.Copy函数的作用是:将源(io.Reader)数据,读取到目标(io.Writer),并一直持续到数据读取完毕 或 出现错误,才会返回读取的字节数 (written int64)与错误信息(err)。

go 复制代码
func Copy(dst io.Writer, src io.Reader) (written int64, err error)

其中dst 为目标写入器,用于接收源数据;src则是源读取器,用于提供数据。

基本用法

go 复制代码
package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	// 打开源文件(只读)
	src, err := os.Open("a.bin")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer src.Close() // 程序结束前关闭源文件句柄

	// 创建/覆盖目标文件(只写)
	dst, err := os.Create("b.bin")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer dst.Close() // 程序结束前关闭目标文件句柄

	// 将 src 的内容拷贝到 dst,直到读完或出错
	written, err := io.Copy(dst, src)
	if err != nil {
		fmt.Println(err)
		return
	}

	// 可选:强制将数据刷到磁盘(更慢;只有强一致需要时才用)
	if err := dst.Sync(); err != nil {
		fmt.Println(err)
		return
	}

	// 打印实际拷贝的字节数
	fmt.Println("copied bytes:", written)
}

这个示例代码,是一个典型的 文件间互相拷贝(file->file)的案例。

把当前目录下的 a.bin文件 复制到 b.bin文件:先打开源文件、创建目标文件,再用 io.Copy 流式拷贝全部内容,最后可选 Sync 强制刷盘,并在终端上输出实际拷贝的字节数。

实现原理

1. 原理

在了解了 io.Copy 的基本定义和使用后,让我带大家对 io.Copy 的实现进行一下深度剖析。

io.Copy 的核心实现分为两步:快路径通用路径

快路径:

io.Copy 在进入"Read(buf) → Write(buf)"通用循环前,会先尝试两条更快的路径:

  • 如果 src(Reader)实现了 io.WriterTo接口,就直接调用 src.WriteTo(dst),让 src 用自己更高效的方式把数据写到 dst
  • 否则如果 dst(Writer)实现了 io.ReaderFrom接口,就调用 dst.ReadFrom(src),让 dst 自己从 src 读取并写入。
    这些快路径的意义是:把拷贝逻辑交给对应类型的所实现的接口,因为配套的接口通常更适合当前的场景,从而避免 io.Copy 自己分配的默认 32KB 临时缓冲区;在某些组合(如文件↔网络)下还可能触发更高效的系统级拷贝(甚至能达到零分配)。

其中 io.WriterToio.ReaderFrom 是 Go 提供的用于优化拷贝的接口,某些 Reader/Writer 类型实现了这些接口以此加快复制速度

通用路径(慢路径)

当 src 和 dst 都不支持上述接口时,就会进入最常见的通用路径:

首先会创建一个临时缓冲区 (默认 32KB;若 src 是 LimitedReader 且剩余更小,会缩小缓冲区),

然后循环执行 src.Read(buf) 把数据读入 缓冲区,再用 dst.Write(buf[:n]) 写出 到具体目标内。

循环持续直到 读完 (也就是Read 返回 EOF)。

这期间如果读或写发生错误,io.Copy 会立刻中断并返回错误。

2. 底层源码:

注:源码截取自Go 1.24版本

go 复制代码
package io

// Copy 将 src 的数据持续读取并写入 dst,直到读完(EOF)或发生错误。
// 返回:实际写入的字节数 written,以及拷贝过程中遇到的错误 err。
func Copy(dst Writer, src Reader) (written int64, err error) {
	// Copy 只是 copyBuffer 的封装:不传 buf 时,由内部决定是否分配默认缓冲区
	return copyBuffer(dst, src, nil)
}

// copyBuffer 是 Copy / CopyBuffer 的核心实现。
// buf 为 nil:内部会分配默认缓冲区;buf 非 nil:直接复用调用方传入的缓冲区。
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
	// 快路径 1:如果 src 自己实现了 WriterTo,则交给 src.WriteTo(dst)(可能更快/少分配)
	if wt, ok := src.(WriterTo); ok {
		return wt.WriteTo(dst)
	}
	// 快路径 2:如果 dst 自己实现了 ReaderFrom,则交给 dst.ReadFrom(src)(可能更快/少分配)
	if rf, ok := dst.(ReaderFrom); ok {
		return rf.ReadFrom(src)
	}

	// 没有传入 buf:分配默认缓冲区(默认 32KB)
	if buf == nil {
		size := 32 * 1024

		// 如果 src 是 LimitedReader,则根据剩余可读字节数缩小 buf(避免浪费内存)
		if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
			if l.N < 1 {
				size = 1
			} else {
				size = int(l.N)
			}
		}
		buf = make([]byte, size)
	}

	// 通用路径:循环 Read -> Write,直到 EOF 或错误
	for {
		// 从 src 读取数据到 buf
		nr, er := src.Read(buf)
		if nr > 0 {
			// 将 buf 中读到的 nr 字节写入 dst
			nw, ew := dst.Write(buf[:nr])

			// 防御性检查:Write 返回值异常(写入负数或写得比读到的还多)视为非法写入
			if nw < 0 || nr < nw {
				nw = 0
				if ew == nil {
					ew = errInvalidWrite
				}
			}

			// 累加已写入字节数
			written += int64(nw)

			// 写入出错:直接结束
			if ew != nil {
				err = ew
				break
			}
			// 短写:读到 nr,但只写了 nw(且无 ew),属于错误
			if nr != nw {
				err = ErrShortWrite
				break
			}
		}

		// Read 返回错误:EOF 表示正常结束;其他错误则返回该错误
		if er != nil {
			if er != EOF {
				err = er
			}
			break
		}
	}
	return written, err
}

项目应用

我最近再编写一套,对大图片进行分片上传断点续传 的接口。

当用到io.Copy的那一刻,我就想通过sync.Pool进行优化。但最终我选择了放弃。

sync.Pool 是Go 提供的临时对象池,可以复用对象以减少GC压力。

如果想深入了解 sync.Pool,可以点击这里(sync.Pool)

在此,我结合自己当时面临的两个抉择,来加深大家对io.Copy的理解。

为何我最初想要进行优化?

因为在io.Copy中的通用路径也就是慢路径中,

通常会make 一个临时缓冲区 (默认32KB的buf),

如下:

go 复制代码
	size := 32 * 1024
	buf = make([]byte, size)

高并发上传图片的情况下,就会导致创建多个buf。也就会造成GC压力过高,而sync.Pool正是解决这个问题的有力工具。

为何我最终直接用 io.Copy(以及为何没上 sync.Pool

io.Copy 并不只是 "固定的 Read(buf)→Write(buf)" 循环。它在进入通用循环前,会先尝试两条快路径

  • 如果 src 实现了 io.WriterTo,优先调用 src.WriteTo(dst)
  • 否则如果 dst 实现了 io.ReaderFrom,调用 dst.ReadFrom(src)

这些快路径的目标是:让更 "懂底层" 的类型(例如文件、网络连接、内存 reader)接管拷贝逻辑,从而减少 io.Copy 自己的分配与搬运开销(不少场景甚至能避免分配 32KB 临时 buf)。

至于为何走了快路径,不需要sync.Pool了,可以看以下所示。
通用路径

src/dst 都不支持上述接口时,io.Copy 才会分配默认 32KB 缓冲区,并循环执行 Read(buf) → Write(buf):

复制代码
	src ──read──> 用户态 buf ──write──> dst
(内核在 read/write 时参与,但 Go 层需要这块 buf 做中转)

快路径

一般调用io.Copy的时候,会先判断是否能直接走快路径(当命中 WriterTo/ReaderFrom 时,拷贝逻辑交由具体类型实现),不能的话再走循环。

go 复制代码
io.Copy(dst, src)
  └─ copyBuffer(dst, src, buf=nil)
       1) if src implements WriterTo   → 走 src.WriteTo(dst)  【快路径1】
       2) else if dst implements ReaderFrom → 走 dst.ReadFrom(src) 【快路径2】
       3) else → 走通用循环(下面)

总结

io.Copy 用于把 src(io.Reader) 的数据持续写入 dst(io.Writer),直到读完(EOF)或出错,并返回写入字节数与错误。实现上会先尝试快路径:

src 实现了 io.WriterTo 则调用 src.WriteTo(dst),否则若 dst 实现了 io.ReaderFrom 则调用 dst.ReadFrom(src)

两者都不支持时才进入通用路径,内部默认分配约 32KB 的缓冲区循环 Read→Write 完成拷贝。


借鉴文章:

1、一文了解io.Copy

2、go标准文档,且源码截取自Go 1.24版本


相关推荐
默|笙2 小时前
【Linux】进程控制(2)进程等待
linux·运维·服务器
jockerzoo@2 小时前
IP 定向灰度发布:ArgoCD + GitLab CLI 方案
运维
乾元2 小时前
红队 / 蓝队:用 AI 自动生成攻击场景并评估防御效果——从“安全演练”到“可计算的网络对抗系统”
运维·网络·人工智能·网络协议·安全·web安全·架构
半路_出家ren2 小时前
Python操作MySQL(详细版)
运维·开发语言·数据库·python·mysql·网络安全·wireshark
lbb 小魔仙2 小时前
eBPF+Linux 6.18:云原生环境下的安全监控与故障排查实战
linux·运维·云原生
Wzx1980122 小时前
go聊天室项目docker部署
运维·docker·容器
_Orch1d2 小时前
非对称加密AKA协议:安全认证与密钥交换
网络·tcp/ip·安全
2301_767902645 小时前
Zabbix
运维·zabbix
Ha_To10 小时前
2025.12.22 OSPF多区域原理与配置方法
网络