切片(slice):动态数组,长度可变。
slice 对标 Python 中的 list。
与 Python 切片的异同
Python 中切片是一种操作,Go 中既是操作也是一种数据类型。
Go 中切片操作比 Python 中弱,不支持设置步长、不支持负数,其他和 Python 差不多。
对数组进行切片操作得到的是切片类型而非数组类型。
var myArr1 [10]int
var mySlice5 = myArr1[1:5] // mySlice5 的类型是 []int
与 Python 不同,Go 中修改切片中的值,原数组也会改变,也就是说 Go 中的切片并不是将原数组中的某一部分复制一份。
基本使用
创建切片
var mySlice1 []int // 中括号中不写长度或 ... 就是切片,虽然语法和数组很相似,但底层完全不一样
var mySlice2 = []int{0, 1, 2}
var mySlice3 = make([]int, 5) // 另一种常用的创建方式。第一个参数指定类型,第二个参数指定切片的初始长度
fmt.Println(len(mySlice3)) // 虽然是个空切片,但长度是 5
var mySlice4 = *new([]int)
添加元素
var slice1 []int
slice1 = append(slice1, 10, 20, 30) // 注意需要重新赋值
slice1 = append(slice1, s2...) // 相当于 Python 中 list 的 extend 方法;相当于 JS 中的数组展开
拷贝切片元素
var slice2 []int
copy(slice2, slice1) // 将 slice1 中的元素拷贝到 slice2。如果 len(slice2) < len(slice1),只拷贝前 len(slice2) 个元素
fmt.Println(slice2) // 上一行中 len(slice2) 为 0,所以经过拷贝后 slice2 仍然是空的
var slice3 = make([]int, len(slice1)) // 此时 make 就派上用场了,因为 make 能指定切片长度
copy(slice3, slice1)
fmt.Println(slice3)
删除元素
Go 语言没有提供直接的删除方式。
需要通过以下方式删除:
slice1 = append(slice1[:2], slice1[3:]...)
上面的操作相当于删除了slice1
的第 2 个元素。这个实现逻辑和 JS 中的splice
方法相似。
判断元素是否存在、查找元素
Go 语言没有提供直接的方法来判断元素是否存在于切片或数组中、查找元素。
需要自己写循环实现。
长度和容量
长度和容量,是不一样的。
slice4 := make([]int, 4) // 创建,并将长度和容量都设为 4
slice4 = append(slice4, 10) // 添加一个元素,长度+1,容量+n
fmt.Println(len(slice4), cap(slice4)) // cap 函数获取容量
slice5 := make([]int, 4, 5) // make 也可以同时设置长度和容量,第三个参数是容量
fmt.Println(len(slice5), cap(slice5))
切片原理
slice 本身是不存值的,它只是一个数据结构,定义大概如下:
type slice struct {
array unsafe.Pointer // 一个指针,指向底层数组的某个位置,代表切片的开头
len int // 长度
cap int // 容量
}
如果直接创建一个 slice,会先在底层创建一个数组,然后创建一个上述数据结构指向该数组。
如果基于一个自己定义的数组通过切片操作创建 slice(即slice := arr[:]
),则只会创建一个上述数据结构指向该数组,不再创建新数组。
arr1 := [10]int{}
slice6 := arr1[2:4] // 注意:通过这种方式创建的切片,其容量为 len(arr1[2:]) 而非 len(arr1[2:4])
扩容策略:当容量小于 1024 时,newCap = 2*oldCap
,当大于 1024 时,newCap = 1.25*oldCap
。
每当容量不够时,会触发扩容,新申请一片空间,将原空间中的值复制过去。(Python 的 list 和 Java 的 ArrayList 都是这样)
了解扩容机制后,下面的现象就可以理解了:
var slc1 = make([]int, 4)
var slc2 = slc1[:]
slc2[0] = 1
fmt.Println(slc1) // slc2 和 slc1 指向同一片空间,改变 slc2 时 slc1 也被改变了,这个很容易理解
slc2 = append(slc2, 0) // slc2 扩容了,底层的新数组是一片新的空间,也就是说 slc2 和 slc1 指向的空间不同了
slc2[2] = 1
fmt.Println(slc1) // 再改变 slc2 时 slc1 就不会改变了
make
的价值之一在于手动指定容量,尽量避免自动扩容,以优化性能。