掌握 Go 的 copy() 函数,避开切片复制里的常见坑

不晚
不晚
发布时间:2026-08-25| 180人浏览过
原创

前言

在 Go 里写切片操作时,`copy()` 经常被当成“直接复制就行”的基础函数,但真正容易出问题的地方,往往是目标长度不够、误以为会自动扩容,或者复制了二维切片后还在共享内部数据。本文从规则、边界和高频场景三部分拆开说明,再结合浅复制与深复制示例,帮你快速判断什么时候能直接用 `copy()`,什么时候必须手动补一层复制逻辑。

在 Go 开发里,切片复制几乎每天都会遇到:扩容、截取、隔离数据、副本修改互不影响,背后都离不开 copy()。但这类代码最容易出错的地方,并不是语法本身,而是长度截断、类型限制以及“看似复制了,实际上还共用内部数据”的问题。下面按使用场景拆开讲清 copy() 的行为和边界,再结合多维切片示例,帮你判断什么时候直接用,什么时候必须自己做深复制。

copy() 到底做了什么

copy() 是 Go 的内置函数,用来把一个切片中的元素复制到另一个切片中,函数签名很直接:

func copy(dst, src []Type) int

其中,dst 是目标切片,src 是源切片,返回值是实际复制的元素个数。它只负责把现有元素搬过去,不负责帮你扩容,也不会替你处理类型转换。

先看一个最基本的例子:

package main

import "fmt"

func main() {
    // 源切片
    sourceSlice := []int{1, 2, 3, 4, 5}
    // 目标切片
    destinationSlice := make([]int, len(sourceSlice))

    // 使用copy函数复制源切片到目标切片
    numCopied := copy(destinationSlice, sourceSlice)

    // 打印结果
    fmt.Println("Source slice:", sourceSlice)
    fmt.Println("Destination slice:", destinationSlice)
    fmt.Println("Number of elements copied:", numCopied)
}

输出如下:

Source slice: [1 2 3 4 5]
Destination slice: [1 2 3 4 5]
Number of elements copied: 5

这里的关键点只有一个:目标切片在复制前必须已经有足够的长度,所以例子里先用 make([]int, len(sourceSlice)) 分配了空间。很多人第一次写错,往往不是忘了 copy(),而是误以为它会顺手把 dst 长出来。

最容易踩坑的 4 个规则

复制数量取 len(dst) 和 len(src) 的较小值

copy() 不会报错,也不会补齐,它只复制两边长度中的较小值。因此目标切片短了,就只能拿到前面那一段:

用外层切片与内层切片的纸面结构图展示浅复制和深复制的差异。
二维切片复制为什么还会互相影响二维切片最能看出 shallow copy 和 deep copy 的区别:外层分开。
用卡片和箭头说明 Go copy() 的复制规则,包括长度取最小值、类型一致和不会自动扩容。
copy() 的复制边界把 copy() 的几个核心限制放到一张图里,比逐段读代码更容易建立判断顺序。
func main() {
    sourceSlice := []int{1, 2, 3, 4, 5}
    destinationSlice := make([]int, 3) // 只分配3个元素的空间

    numCopied := copy(destinationSlice, sourceSlice)

    fmt.Println("Copied elements:", numCopied) // Output: 3
    fmt.Println("Destination slice:", destinationSlice) // Output: [1 2 3]
}

这也是为什么判断复制是否完整时,返回值很有意义。只要返回值小于你预期的元素数,就说明目标切片长度不够,或者源数据本身就没那么多。

源切片和目标切片的元素类型必须一致

copy() 不做隐式转换。元素类型不一致时,会直接在编译阶段报错:

用分栏卡片展示 copy() 的常见应用,包括扩容、截取独立子切片、隔离底层数组和复制字符串到字节切片。
copy() 的常见落地场景这张图不重复讲规则,而是把 copy() 在业务代码里最常见的四种落地方式做成速查卡。
src := []int{1, 2, 3}
dst := make([]float64, len(src))
n := copy(dst, src) // 编译错误:类型不匹配

如果确实要在不同类型之间搬数据,就不能指望 copy() 一步完成,必须先显式转换成对应类型。

copy() 不会自动扩容

很多人把它和 append() 混在一起理解,这是两个完全不同的动作。append() 可能触发扩容,而 copy() 只在目标切片现有长度范围内工作。换句话说,容量够不够不重要,长度够不够才重要。

如果你只写了容量而没有把长度拉起来,copy() 仍然复制不到那些“预留空间”里。实际开发里要么直接用 make([]T, len(src)),要么先把目标切片切到需要的长度再复制。

复制不会修改源切片

copy() 的动作发生在目标切片上,源切片内容不会被改写。对于元素本身是值类型的情况,复制完成后,两边后续互不影响。

不过这句话有一个前提:元素如果是引用类型,表面上“源切片没变”,内部引用指向的内容却仍然可能被共同访问。这个问题要放到浅复制和深复制里看。

浅复制和深复制,差别到底在哪

copy() 默认做的是浅复制。这个结论在一维基本类型切片里不明显,但到了二维切片、切片里套切片、或者元素中包含 map、指针时,就会直接影响结果。

浅复制:外层切片分开了,内部元素未必分开

对于 int、string、float 这类基本数据类型,复制后通常可以把两个切片看成独立副本。但如果元素本身是引用类型,复制过去的其实是“引用”。

下面这个二维切片例子最能说明问题:

func main() {
    matA := [][]int{
        {0, 1, 1, 0},
        {0, 1, 1, 1},
        {1, 1, 1, 0},
    }
    matB := make([][]int, len(matA))
    copy(matB, matA)

    // matA和matB的地址不同,但matA[0]和matB[0]的地址相同!
    fmt.Printf("%p, %pn", matA, matA[0]) // 0x14000130000, 0x1400012e020
    fmt.Printf("%p, %pn", matB, matB[0]) // 0xc00012a0500, 0x1400012e020
}

从打印结果可以看出,matA 和 matB 这两个外层切片的地址不同,但第一行内部切片的地址相同。这说明 copy(matB, matA) 只复制了外层元素,而外层元素本身是 []int,属于引用语义的数据结构,所以内部行数据仍然共享。

这类情况最危险的地方在于:代码表面像是做了备份,实际一改 matB[0][0],matA[0][0] 也会跟着变。

深复制:逐层分配,再逐层复制

如果你的目标是彻底断开关联,就不能只复制最外层。对于多维切片,正确做法是先为每一层重新分配空间,再分别调用 copy():

func main() {
    matA := [][]int{
        {0, 1, 1, 0},
        {0, 1, 1, 1},
        {1, 1, 1, 0},
    }
    matB := make([][]int, len(matA))

    for i := range matA {
        matB[i] = make([]int, len(matA[i])) // 注意初始化长度
        copy(matB[i], matA[i])
    }

    // 现在matA和matB的地址以及内部切片的地址都不同了
    fmt.Printf("%p, %pn", matA, matA[0]) // 0x1400013c000, 0x1400013a020
    fmt.Printf("%p, %pn", matB, matB[0]) // 0x1400013c050, 0x1400013a080
}

这里不能少的是这一句:

matB[i] = make([]int, len(matA[i]))

如果只声明外层切片,却不初始化每个内部切片的长度,后面的 copy(matB[i], matA[i]) 其实复制不进去任何东西。判断是否需要深复制,核心标准只有一个:你是否允许副本继续共享内部数据。

4 个高频场景,什么时候该直接用 copy()

切片扩容时保留旧数据

当现有切片空间不够,想把数据迁移到更大的切片里时,copy() 是最常见的办法:

func main() {
    oldSlice := []int{1, 2, 3}
    newSlice := make([]int, len(oldSlice)*2) // 双倍扩容
    copy(newSlice, oldSlice)
    fmt.Println("New slice:", newSlice) // [1 2 3 0 0 0]
}

这个写法本质上就是“新建一块更大的空间,再把旧数据搬过去”。复制后多出来的位置会保留零值,因此结果里后半段是 0。

提取切片中的一段内容

如果你不想继续和原切片共享底层数组,而是想拿到某一段数据的独立副本,可以先切片,再复制:

func main() {
    src := []int{1, 2, 3, 4, 5}
    dst := make([]int, 3)
    copy(dst, src[1:4]) // 复制src中索引1到4的元素(不包含索引4)
    fmt.Println("Destination slice:", dst) // [2 3 4]
}

和直接写 sub := src[1:4] 相比,这种方式得到的是独立数据,不会因为后续修改而和原切片互相影响。

避免多个切片共享同一个底层数组

这是 copy() 在业务代码里最实用的一类场景。很多 Bug 都来自“看起来是新切片,实际上还指着原来的底层数组”。想明确隔离,就直接复制一份:

func main() {
    src := []int{1, 2, 3, 4, 5}
    dst := make([]int, len(src))
    copy(dst, src)
    dst[0] = 100 // 修改dst不会影响src
    fmt.Println("Source:", src) // [1 2 3 4 5]
    fmt.Println("Destination:", dst) // [100 2 3 4 5]
}

这里 dst[0] = 100 不会改动 src,前提正是 dst 已经拥有自己的底层数据空间。

把字符串复制到字节切片

copy() 不只用于切片之间复制,也可以把字符串内容复制到 []byte 中:

func main() {
    src := "Hello, World!"
    dst := make([]byte, len(src))
    n := copy(dst, src)
    fmt.Println("Copied elements:", n) // 13
    fmt.Println("Destination slice:", dst) 
    // [72 101 108 108 111 44 32 87 111 114 108 100 33]
}

返回值 n 依然表示实际复制的元素数量,这里是 13。最终得到的是字节值,而不是字符串形式本身。

性能表现和实战判断

copy() 之所以值得优先使用,还有一个很现实的原因:它是 Go 运行时高度优化过的内置函数。处理大块数据时,底层通常会直接走高效的内存复制逻辑,性能往往明显好于手写循环逐个赋值。

不过性能不是唯一判断标准。实战里更重要的是先分清下面三件事:

  • 目标切片长度是否足够,返回值是否符合预期;
  • 复制后是否需要与源数据彻底隔离;
  • 元素是不是引用类型,是否会触发“只复制外层”的浅复制问题。

把这三点理顺之后,copy() 就不会只是一个“会用”的函数,而是一个能帮你规避切片共享、扩容迁移和多维数据复制坑点的基础工具。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。