概述
Go 语言中切片的复制是非常重要也比较容易让新手困惑的问题。本文将通过大量示例代码,全面介绍切片复制的相关知识,包括:
切片的结构
copy()函数的用法
切片复制的本质
浅复制和深复制的区别
如何实现切片深复制
copy()函数的常见用途
切片复制需要注意的几点
1. 切片的结构
在讲解切片复制之前,我们先快速回顾下切片的结构。
切片是对数组的抽象和封装,所以切片实际上是一个包含三个字段的结构体:
type slice struct {
array *[ ]Type
len int
cap int
}
array 指向底层数组
len 记录可用元素数量
cap 记录总容量
举个例子:
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // s引用arr的部分数据
这个切片 s 的结构大致如下:
s.array = &arr
s.len = 2
s.cap = 4
数组是值类型,但切片作为引用类型,所以切片之间赋值或传参时,只会复制引用,底层数组同一块内存被共享。
2. copy()函数用法
Go 语言内置的 copy()函数可以用于切片之间元素的复制,函数签名如下:
func copy(dst, src []Type) int
copy()会将 src 切片中的元素复制到 dst 中,复制长度以 len(src)和 len(dst)的最小值为准。它返回复制的元素个数。
使用 copy()复制切片:
s1 := []int{1, 2, 3}
s2 := make([]int, 10)
n := copy(s2, s1)
fmt.Println(s1, s2, n) // [1 2 3] [1 2 3 0 0 0 0 0 0 0] 3
这里我们把 s1 切片中的 3 个元素复制到 s2 中,n 返回复制的元素个数 3。
需要注意的是,copy()会先计算 dst 的长度 l=len(dst),再计算复制长度 n=min(len(src), l)。
再看一个例子:
s1 := []int{1, 2, 3}
s2 := make([]int, 2)
n := copy(s2, s1)
fmt.Println(s1, s2, n) // [1 2 3] [1 2] 2
s2 的长度只有 2,所以只复制了 s1 的前 2 个元素,n 返回 2。
3. 切片复制的本质
Go 语言中切片之间复制实际上是“引用的复制”,而不是值的复制。
也就是说,复制的是底层数组的引用,底层数组本身并没有复制。复制前后,src 和 dst 切片引用的是同一底层数组。
s1 := []int{1, 2, 3}
s2 := make([]int, 3)
copy(s2, s1)
fmt.Println(&s1[0], &s2[0]) // 0xc0000180a8 0xc0000180a8
可以看到,s1 和 s2 的底层数组地址是一样的。修改 s2 会影响 s1:
s2[0] = 100
fmt.Println(s1) // [100 2 3]
这种复制属于浅复制(shallow copy),类似于 C 语言中的 memcpy,只复制数组指针和相关属性。
4. 浅复制和深复制
根据复制的层次,可以将切片复制分为浅复制和深复制。
浅复制:只复制切片的基本数据,底层数组共享
深复制:复制切片及底层数组,break 引用关系
上面 copy()函数实现的是浅复制,如果需要深复制,需自己实现。
4.1 浅复制
浅复制只复制切片本身,底层数组共享,修改一个切片会影响另一个:
func main() {
s1 := []int{1, 2, 3}
s2 := shallowCopy(s1) // 浅复制
s2[0] = 100
fmt.Println(s1) // [100 2 3]
}
func shallowCopy(src []int) []int {
dst := make([]int, len(src))
copy(dst, src)
return dst
}
浅复制对元素包含指针的切片也是问题:
type User struct {
id int
name *string
}
func main() {
u1 := User{1, &name}
u2 := shallowCopy([]User{u1})
*u2[0].name = "newName" // 修改了u1.name
}
4.2 深复制
深复制需要自己实现,完全 break 底层数组引用关系:
func deepCopy(src []int) []int {
dst := make([]int, len(src))
for i := range src {
dst[i] = src[i]
}
return dst
}
这样修改 dst 不会影响到 src。
对于包含指针的切片,需要额外处理指针指向的内容。
5. 切片深复制实现
下面介绍几种实现切片深复制的方法。
5.1 手动循环赋值
可以通过手动循环一个个元素进行深复制:
func copyDeep(dst, src []int) {
for i := range src {
dst[i] = src[i]
}
}
类似 for 循环的方式也可以用于自定义类型:
type User struct {
id int
name string
}
func copyUserDeep(dst, src []User) {
for i := range src {
dst[i].id = src[i].id
dst[i].name = src[i].name
}
}
手动循环虽然稍微繁琐,但是性能和可控性较好。
5.2 利用反射
Go 语言反射可以自动深复制任意类型,但是需要注意反射带来的性能损耗:
import "reflect"
func copyDeep(dst, src interface{}) {
dv := reflect.ValueOf(dst).Elem()
sv := reflect.ValueOf(src).Elem()
for i := 0; i < sv.NumField(); i++ {
fd := dv.Field(i)
if fd.CanSet() {
fd.Set(sv.Field(i))
}
}
}
使用时:
var s1 []int = []int{1, 2, 3}
s2 := make([]int, 3)
copyDeep(&s2, &s1)
反射的威力在于可以处理任意类型,但是需要注意反射带来的额外性能损耗。
5.3 利用 encoding/gob
gob 是一个二进制数据序列化的格式,可以用于深度 Copy:
import (
"bytes"
"encoding/gob"
)
func copyDeep(src, dst interface{}) error {
buff := new(bytes.Buffer)
enc := gob.NewEncoder(buff)
dec := gob.NewDecoder(buff)
if err := enc.Encode(src); err != nil {
return err
}
if err := dec.Decode(dst); err != nil {
return err
}
return nil
}
使用 encoding/gob 进行深拷贝也有一定的性能损耗。
5.4 利用第三方库
如果需要频繁深拷贝,可以考虑使用一些第三方库,如:
github.com/jinzhu/copier
github.com/ulule/deepcopier
这些库利用反射实现泛型深拷贝,并进行了性能优化,会更高效。
6. copy()函数的常见用途
copy()作为切片浅复制的主要函数,使用场景还是很多的,主要有:
切片扩容时复制老数据
从一个切片截取部分元素到新切片
切片重组,两个切片交换元素
将字节流复制到字节切片缓冲
文件拷贝等
6.1 切片扩容
Go 语言中切片扩容时,常用 copy()来复制老数据:
func appendSlice(slice []int) []int {
newSlice := make([]int, len(slice)+1)
copy(newSlice, slice)
return newSlice
}
6.2 截取切片
从一个大切片截取需要的部分到新切片:
bytes := []byte("Hello World")
hello := make([]byte, 5)
copy(hello, bytes[:5])
world := make([]byte, 5)
copy(world, bytes[6:])
6.3 切片重组
两个切片可以通过 copy 相互交换元素:
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
copy(s1, s2)
copy(s2, s1)
交换后 s1=[4,5,3],s2=[1,2]。
6.4 字节流复制
IO 操作读取字节流时,常用 copy()写入字节切片缓冲:
buf := make([]byte, 1024)
for {
n, err := r.Read(buf)
// 使用buf前N字节
}
6.5 文件复制
利用 copy()可以实现高效的文件拷贝:
func CopyFile(dst, src string) error {
r, w := os.Open(src), os.Create(dst)
defer r.Close(); defer w.Close()
buf := make([]byte, 1024*1024)
for {
n, err := r.Read(buf)
if err != nil {
if err == io.EOF {
break
}
return err
}
if n == 0 {
break
}
w.Write(buf[:n])
}
return nil
}
7. 注意事项
最后需要注意几点:
copy()要求 dst 必须提前分配内存,否则会 panic
指针或包含指针的切片只会复制指针,不会深复制目标对象
多次复制切片会造成 GC 负担,尽量复用内存减少不必要的复制
8. 思考题
描述下切片的结构包含哪些字段
copy()函数签名是什么
切片复制的本质是什么
如何实现切片的深复制
copy()函数有哪些常见的使用场景