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


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() 不做隐式转换。元素类型不一致时,会直接在编译阶段报错:

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() 就不会只是一个“会用”的函数,而是一个能帮你规避切片共享、扩容迁移和多维数据复制坑点的基础工具。









