Go新手别再被切片复制坑了

2024-01-09 17:47:59

概述

Go 语言中切片的复制是非常重要也比较容易让新手困惑的问题。本文将通过大量示例代码,全面介绍切片复制的相关知识,包括:

  1. 切片的结构

  2. copy()函数的用法

  3. 切片复制的本质

  4. 浅复制和深复制的区别

  5. 如何实现切片深复制

  6. copy()函数的常见用途

  7. 切片复制需要注意的几点

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 = &arrs.len = 2s.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] = 100fmt.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()函数有哪些常见的使用场景

文章来源:https://blog.csdn.net/m0_37723088/article/details/135483097
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。