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


目录
相关文章
|
4天前
|
Linux API 数据安全/隐私保护
OpenClaw零门槛部署手册:阿里云+本地多系统搭建+免费大模型配置流程+常见问题解析
OpenClaw(昵称“龙虾”)作为2026年热门的开源AI框架,区别于传统聊天型AI,其核心价值在于能够自动执行任务、操作设备、处理文件、浏览网页,成为真正意义上的“数字员工”。该工具完全开源,支持多平台部署,既可以通过阿里云实现云端7×24小时稳定运行,也能部署在本地MacOS、Linux、Windows11设备上保障数据隐私。本文将详细拆解2026年OpenClaw的部署流程,包括阿里云轻量应用服务器一键部署、ECS手动部署,以及本地三大系统的安装步骤,同时提供阿里云百炼免费API与Coding Plan免费大模型的配置方法,并整理部署与使用中的常见问题,帮助新手零门槛上手。
230 6
|
7天前
|
前端开发 JavaScript 开发者
前端开发:不写样式代码才是最好的写样式方式
本文揭示前端开发中“重样式、轻业务”的困局,指出CSS调试耗时低效、易出错且价值难被认可。提出通过架构层建设统一的样式体系——涵盖组件库、样式库、mixin、变量、PostCSS与Stylelint,让业务开发者少写甚至不写CSS,专注核心逻辑,实现高效、一致、可维护的前端研发。
325 124
|
4天前
|
人工智能 安全 数据挖掘
《【脑洞】想做一个基于Qwen的“启灵麒麟”智能体,寻找志同道合的开发者/策划》
“启灵”麒麟是中国AI国家级文化IP战略:以“启灵”为名,赋予AI中文灵魂;以“麒麟”为相,承载仁智祥瑞的文化图腾。通过智能玩偶、科普动画、开放世界游戏构建虚实融合的“启灵宇宙”,推动阿里从技术提供商跃升为AI文化定义者。
|
2天前
|
人工智能 Linux iOS开发
OpenClaw+QMT‑MCP量化交易实战:AI交易员全流程部署、模型配置与自动交易实现(附阿里云/Windows/macOS/Linux部署OpenClaw教程)
在量化交易领域,自动化执行与策略智能化已成为主流方向。OpenClaw(Clawdbot)作为开源AI Agent框架,可充当交易系统的“大脑”,负责理解指令、分析行情、拆解逻辑、规划执行;QMT‑MCP则遵循MCP(Model Context Protocol)协议,将本地QMT交易客户端封装为标准接口,成为AI交易员的“执行双手”,完成下单、撤单、查询持仓、查询资产等真实交易操作。
310 7
|
4天前
|
人工智能 Linux API
9.9元上手OpenClaw超级助理:阿里云一键部署+本地搭建+百炼API配置+避坑指南
2026年,AI已经从“辅助聊天”进化为“真正替人干活”的执行智能体,而OpenClaw(曾用名Clawdbot、Moltbot,俗称龙虾AI)正是这场变革中的标杆产品。它不再是只能给出文字建议的对话工具,而是能直接操控设备、处理文件、收发邮件、编写代码、管理日程、抓取数据的全天候数字员工。为了让普通用户也能零门槛用上这款强大工具,阿里云在2026年3月推出OpenClaw专属一键部署方案,新用户首月仅需9.9元,即可拥有2核2G、40GB ESSD、200Mbps带宽的预装环境,10分钟内完成全流程搭建。
136 7
|
4天前
|
前端开发
前端开发 之 15个页面加载特效下【附完整源码】
本篇文章内容展示了铜钱3D圆环加载、圆环显现加载、扇形百分比加载等页面炫酷加载特效,并给出了完整的代码及注释
69 8
|
1天前
|
存储 缓存 Java
为何最终我放弃了 Go 的 sync.Pool
本文并非否定 sync.Pool,而是分享技术选型的思考过程,帮助大家更准确地使用它
28 1
|
1天前
|
存储 安全 前端开发
双Token的致命漏洞,你的系统安全吗?
你的双Token实现真的安全吗?很多人不知道,经典的双Token方案中其实藏着致命的漏洞。
33 2
|
1天前
|
设计模式 Java Go
Go中的switch的8种使用场景:没有你想的那么简单
在 Go 中灵活使用 switch,可以使代码更清晰、更易维护。 switch 是 Go 中不可或缺的控制结构之一
22 0
|
1天前
CLion中文乱码的解决方案
CLion中文乱码的解决方案
11 1