Golang-Slice切片

Slice是Go语言提供的一种常用的动态数据结构,相比数组,其长度并不固定,当我们向切片中添加元素时,如果容量不足就会自动扩容。

可以通过三种方式声明一个切片:

  1. 利用下标获取数组或切片的一部分
  2. 使用字面量初始化新的切片
  3. 使用关键字make创建切片
1
2
3
4
5
6
//1
mySlice := slice[1:3]
//2
mySlice := []int{1,2,3}
//3
mySlice := make([]int,0,3)

切片在编译期间生成的类型只会包含元素类型,如intinterface{}等。

数据结构

运行期间的Slice数据结构包含三个部分:

  • Data:指向数组的指针
  • Len:当前切片的长度
  • Cap:当前切片的容量

从切片的结构中可以看出,Slice是对底层数据的一层抽象引用,上层只需要和切片交互而不关心底层数组的变化

扩容

我们可以通过append向切片中添加元素,如果追加元素后切片大小超过容量,那么就会触发自动扩容。注意使用slice = append(slice,1,2,3)这种追加方式使得扩容后的切片覆盖原切片。

当切片容量不足时,就会为切片分配新的内存空间并拷贝原切片中的元素,运行时根据切片的当前容量选择不同的扩容策略:

  1. 如果期望容量大于当前容量两倍,则使用期望容量;

  2. 如果当前切片长度小于1024,则将容量翻倍;

  3. 如果切片长度大于1024,则每次增加25%的容量,直到新容量大于期望容量。

此外,还需根据切片中元素大小进行内存对齐,将待申请的内存进行向上取整。

1
2
var arr []int64
arr = append(arr, 1, 2, 3, 4, 5)

如上述Slice中期望容量为40字节,但因为切片中的元素大小等于 sys.PtrSize,会向上取整申请内存大小为48字节,新切片容量为6.

拷贝

Go语言中的拷贝分为浅拷贝和深拷贝,对于像=[:]下标方式对切片的浅拷贝操作,只需要拷贝三个字段;而通过Go语言内置的copy()函数,则会进行深拷贝,涉及对底层数组的拷贝,理论上开销更大。

遍历

Go语言提供了range用于for循环遍历Slice,主要有两种方式:

1
2
3
4
// 遍历下标和对应值
for k,v := range slice{}
// 遍历下标
for k := range slice{}

使用range遍历切片时,变量v是切片中数据的拷贝,因此修改拷贝数据不会影响原切片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type User struct {
Age int
Name string
}

func main() {
user := []User{
{Age: 18, Name: "flbu920"},
}
for _, v := range user {
v.Age = 20
}
fmt.Println(user)
}
// print
[{18 flbu920}]

参数传递

Slice在作为函数参数传递时应特别注意,Go语言参数传递只有值传递,传递一个切片就会浅拷贝原切片,但因为指向的是同一个底层数组,函数内对切片元素的修改会影响到函数外的切片,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
s := []string{"flbu920", "努力赚钱"}
modifySlice(s)
fmt.Println("outSlice: ", s)
}

func modifySlice(s []string) {
s[0] = "卜飞龙"
s[1] = "开摆开摆"
fmt.Println("innerSlice: ", s)
}
//print:
innerSlice: [卜飞龙 开摆开摆]
outSlice: [卜飞龙 开摆开摆]

但如果切片指向底层数组的指针被覆盖或修改(copy、append触发扩容),则此时函数内部和外部的Slice不再指向同一个数组,对函数内Slice的修改不会影响外部的Slice。

-------------本文结束感谢您的阅读-------------