切片(slice):动态数组,长度可变。
slice 对标 Python 中的 list。

与 Python 切片的异同

Python 中切片是一种操作,Go 中既是操作也是一种数据类型。

Go 中切片操作比 Python 中弱,不支持设置步长、不支持负数,其他和 Python 差不多。

对数组进行切片操作得到的是切片类型而非数组类型

  1. var myArr1 [10]int
  2. var mySlice5 = myArr1[1:5] // mySlice5 的类型是 []int

与 Python 不同,Go 中修改切片中的值,原数组也会改变,也就是说 Go 中的切片并不是将原数组中的某一部分复制一份。

基本使用

创建切片

  1. var mySlice1 []int // 中括号中不写长度或 ... 就是切片,虽然语法和数组很相似,但底层完全不一样
  2. var mySlice2 = []int{0, 1, 2}
  3. var mySlice3 = make([]int, 5) // 另一种常用的创建方式。第一个参数指定类型,第二个参数指定切片的初始长度
  4. fmt.Println(len(mySlice3)) // 虽然是个空切片,但长度是 5
  5. var mySlice4 = *new([]int)

添加元素

  1. var slice1 []int
  2. slice1 = append(slice1, 10, 20, 30) // 注意需要重新赋值
  3. slice1 = append(slice1, s2...) // 相当于 Python 中 list 的 extend 方法;相当于 JS 中的数组展开

拷贝切片元素

  1. var slice2 []int
  2. copy(slice2, slice1) // 将 slice1 中的元素拷贝到 slice2。如果 len(slice2) < len(slice1),只拷贝前 len(slice2) 个元素
  3. fmt.Println(slice2) // 上一行中 len(slice2) 为 0,所以经过拷贝后 slice2 仍然是空的
  4. var slice3 = make([]int, len(slice1)) // 此时 make 就派上用场了,因为 make 能指定切片长度
  5. copy(slice3, slice1)
  6. fmt.Println(slice3)

删除元素

Go 语言没有提供直接的删除方式。

需要通过以下方式删除:

  1. slice1 = append(slice1[:2], slice1[3:]...)

上面的操作相当于删除了slice1的第 2 个元素。这个实现逻辑和 JS 中的splice方法相似。

判断元素是否存在、查找元素

Go 语言没有提供直接的方法来判断元素是否存在于切片或数组中、查找元素。

需要自己写循环实现。

长度和容量

长度和容量,是不一样的。

  1. slice4 := make([]int, 4) // 创建,并将长度和容量都设为 4
  2. slice4 = append(slice4, 10) // 添加一个元素,长度+1,容量+n
  3. fmt.Println(len(slice4), cap(slice4)) // cap 函数获取容量
  4. slice5 := make([]int, 4, 5) // make 也可以同时设置长度和容量,第三个参数是容量
  5. fmt.Println(len(slice5), cap(slice5))

切片原理

slice 本身是不存值的,它只是一个数据结构,定义大概如下:

  1. type slice struct {
  2. array unsafe.Pointer // 一个指针,指向底层数组的某个位置,代表切片的开头
  3. len int // 长度
  4. cap int // 容量
  5. }

如果直接创建一个 slice,会先在底层创建一个数组,然后创建一个上述数据结构指向该数组。

如果基于一个自己定义的数组通过切片操作创建 slice(即slice := arr[:]),则只会创建一个上述数据结构指向该数组,不再创建新数组。

  1. arr1 := [10]int{}
  2. slice6 := arr1[2:4] // 注意:通过这种方式创建的切片,其容量为 len(arr1[2:]) 而非 len(arr1[2:4])

扩容策略:当容量小于 1024 时,newCap = 2*oldCap,当大于 1024 时,newCap = 1.25*oldCap

每当容量不够时,会触发扩容,新申请一片空间,将原空间中的值复制过去。(Python 的 list 和 Java 的 ArrayList 都是这样)

了解扩容机制后,下面的现象就可以理解了:

  1. var slc1 = make([]int, 4)
  2. var slc2 = slc1[:]
  3. slc2[0] = 1
  4. fmt.Println(slc1) // slc2 和 slc1 指向同一片空间,改变 slc2 时 slc1 也被改变了,这个很容易理解
  5. slc2 = append(slc2, 0) // slc2 扩容了,底层的新数组是一片新的空间,也就是说 slc2 和 slc1 指向的空间不同了
  6. slc2[2] = 1
  7. fmt.Println(slc1) // 再改变 slc2 时 slc1 就不会改变了

make的价值之一在于手动指定容量,尽量避免自动扩容,以优化性能。