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


目录
相关文章
|
26天前
|
机器学习/深度学习 人工智能 Java
优先队列 priority_queue详解
说到,priority_queue优先队列。必须先要了解啥是堆与运算符重载(我在下方有解释)。否则只知皮毛,极易忘记==寸步难行。但在开头,还是简单的说下怎么应用。
566 1
|
26天前
|
消息中间件 NoSQL Redis
高可靠微服务消息设计:Outbox模式、延迟队列与Watermill集成实践
构建高可靠微服务,事件丢失和延迟任务一直是难题?本文带你从实战角度掌握 Outbox模式、延迟队列 及 Watermill+Redis Stream 集成方案,教你用Go打造可靠、可观测、毫秒级响应的事件驱动系统。
163 2
|
26天前
|
资源调度 运维 供应链
【多微电网】计及碳排放的基于交替方向乘子法(ADMM)的多微网电能交互分布式运行策略研究附Matlab代码
​ ✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。 🍎 往期回顾关注个人主页:Matlab科研工作室 👇 关注我领取海量matlab电子书和数学建模资料 🍊个人信条:格物致知,完整Matlab代码获取及仿真咨询内容私信。 🔥 内容介绍 一、研究背景 电动汽车市场的蓬勃发展 电力系统面临的挑战 二、用户充电负荷与最优分时电价互动的意义 优化电网负荷曲线 提升用户经济效益 三、光储充换电站的关键组成部分及作用 光伏发电系统 储能系统 充电与换电设施 四、优化模型的构建思路 目
317 123
|
24天前
|
机器学习/深度学习 人工智能 缓存
Alibaba Cloud Linux 4 LTS 64位 Deb 版是什么系统镜像?兼容Debian和Ubuntu吗?
Alibaba Cloud Linux 4 LTS 64位Deb版是阿里云首个兼容Debian生态的LTS系统,深度适配Ubuntu 24.04,专为AI/深度学习优化。预装KeenTune智能调优框架、AI加速内核及kmod-fuse,支持百万IOPS与40GB/s缓存带宽,提供2025–2038年长期支持。(239字)
|
26天前
|
前端开发 JavaScript 应用服务中间件
手把手教你给项目配 HTTPS(Nginx 实战教程,前端 + 后端)
本文章中你既能收获"为什么",也会收获"怎么做"。
302 5
手把手教你给项目配 HTTPS(Nginx 实战教程,前端 + 后端)
|
26天前
|
缓存 安全 测试技术
GO项目开发规范文档解读
本篇博客的目的,更多是为快速翻阅与回忆使用。
175 1
|
24天前
|
人工智能 安全 机器人
企业OpenClaw部署实践:基于阿里云无影一键部署方案
OpenClaw(原Clawdbot/Moltbot)是一款开源本地优先AI智能体平台,支持自然语言调用浏览器、邮件、文件等工具,自动处理文档、日程、邮件等任务。阿里云提供一键部署方案,尤其推荐无影云电脑版——集中管理、多端接入、7×24稳定运行、数据不出域、开箱即用。
310 15
|
27天前
|
人工智能 Linux iOS开发
OpenClaw+QMT‑MCP量化交易实战:AI交易员全流程部署、模型配置与自动交易实现(附阿里云/Windows/macOS/Linux部署OpenClaw教程)
在量化交易领域,自动化执行与策略智能化已成为主流方向。OpenClaw(Clawdbot)作为开源AI Agent框架,可充当交易系统的“大脑”,负责理解指令、分析行情、拆解逻辑、规划执行;QMT‑MCP则遵循MCP(Model Context Protocol)协议,将本地QMT交易客户端封装为标准接口,成为AI交易员的“执行双手”,完成下单、撤单、查询持仓、查询资产等真实交易操作。
1434 7
|
26天前
|
设计模式 Java Go
Go中的switch的8种使用场景:没有你想的那么简单
在 Go 中灵活使用 switch,可以使代码更清晰、更易维护。 switch 是 Go 中不可或缺的控制结构之一
794 0