/ Go 语言切片复制完全指南 /
Go 语言中切片的复制是非常重要也比较容易让新手困惑的问题。本文将通过大量示例代码,全面介绍切片复制的相关知识,包括:
- 切片的结构
- copy()函数的用法
- 切片复制的本质
- 浅复制和深复制的区别
- 如何实现切片深复制
- copy()函数的常见用途
- 切片复制需要注意的几点
希望通过本文的学习,能够帮助大家深入理解 Go 语言中切片复制的各种用法和技巧,在开发中能够灵活运用。文末还准备了思考题,可以检验自己的学习效果。
1
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
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
需要注意的是,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
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
4. 浅复制和深复制
根据复制的层次,可以将切片复制分为浅复制和深复制。
- 浅复制:只复制切片的基本数据,底层数组共享
- 深复制:复制切片及底层数组,break 引用关系
上面 copy()函数实现的是浅复制,如果需要深复制,需自己实现。
4.1
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
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. 切片深复制实现
下面介绍几种实现切片深复制的方法。
5.1
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
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
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
5.4 利用第三方库
如果需要频繁深拷贝,可以考虑使用一些第三方库,如:
- github.com/jinzhu/copier
- github.com/ulule/deepcopier
这些库利用反射实现泛型深拷贝,并进行了性能优化,会更高效。
6
6. copy()函数的常见用途
copy()作为切片浅复制的主要函数,使用场景还是很多的,主要有:
- 切片扩容时复制老数据
- 从一个切片截取部分元素到新切片
- 切片重组,两个切片交换元素
- 将字节流复制到字节切片缓冲
- 文件拷贝等
6.1
6.1 切片扩容
Go 语言中切片扩容时,常用 copy()来复制老数据:
func appendSlice(slice []int) []int { newSlice := make([]int, len(slice)+1) copy(newSlice, slice) return newSlice }
6.2
6.2 截取切片
从一个大切片截取需要的部分到新切片:
bytes := []byte("Hello World") hello := make([]byte, 5) copy(hello, bytes[:5]) world := make([]byte, 5) copy(world, bytes[6:])
6.3
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
6.4 字节流复制
IO 操作读取字节流时,常用 copy()写入字节切片缓冲:
buf := make([]byte, 1024) for { n, err := r.Read(buf) // 使用buf前N字节 }
6.5
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
7. 注意事项
最后需要注意几点:
- copy()要求 dst 必须提前分配内存,否则会 panic
- 指针或包含指针的切片只会复制指针,不会深复制目标对象
- 多次复制切片会造成 GC 负担,尽量复用内存减少不必要的复制
8
思考题
- 描述下切片的结构包含哪些字段
- copy()函数签名是什么
- 切片复制的本质是什么
- 如何实现切片的深复制
- copy()函数有哪些常见的使用场景
欢迎在评论区分享你的思考和见解。