io.copy

简介: 本文将会从定义、用法、底层源码逐一来讲解。并在文末通过项目见闻,来加深大家的io.Copy的理解与思考。

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

基本定义

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

func Copy(dst io.Writer, src io.Reader) (written int64, err error)

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

基本用法

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版本

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),
如下:

    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 时,拷贝逻辑交由具体类型实现),不能的话再走循环。

io.Copy(dst, src)
  └─ copyBuffer(dst, src, buf=nil)
       1) if src implements WriterTo   → 走 src.WriteTo(dst)  【快路径12) else if dst implements ReaderFrom → 走 dst.ReadFrom(src) 【快路径23) 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版本


目录
相关文章
|
1天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
10097 24
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
13天前
|
人工智能 安全 Linux
【OpenClaw保姆级图文教程】阿里云/本地部署集成模型Ollama/Qwen3.5/百炼 API 步骤流程及避坑指南
2026年,AI代理工具的部署逻辑已从“单一云端依赖”转向“云端+本地双轨模式”。OpenClaw(曾用名Clawdbot)作为开源AI代理框架,既支持对接阿里云百炼等云端免费API,也能通过Ollama部署本地大模型,完美解决两类核心需求:一是担心云端API泄露核心数据的隐私安全诉求;二是频繁调用导致token消耗过高的成本控制需求。
5828 14
|
21天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
22759 119