Go语言切片 | 青训营笔记

75 阅读4分钟

Go 语言切片是对数组的一个连续片段的引用,所以切片是一个引用类型。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片声明

  1. 可以 声明 一个 未指定大小的数组 来定义切片,切片声明时不需要说明长度。
var sliceName []type

[]没有声明长度,说明这是一个切片,而不是一个数组。因为数组声明是必须指定长度的。

只声明不进行初始化,切片的默认值为 nil ,之所以为 nil ,是因为 没有分配存储空间。

var slice []int
if slice == nil{
    fmt.Println("切片默认类型为 nil")
}else{
    fmt.Println("切片默认类型不为 nil")
}

输出结果:

切片默认类型为 nil
  1. 可以通过 make() 函数声明切片

make() 是 go 语言的内置函数,可以使用它来声明切片,具体格式如下:

make( []Type, size, cap )
  • Type:切片的元素类型
  • size:为该元素分配多少元素
  • cap:可选参数,预分配的元素容量
s := make([]int,3)
fmt.Println(s)  //  [0,0,0]
​
s1 := make([]int,3,10)
fmt.Println(s1)  //  [0,0,0]

在未进行赋值的情况下生成的切片默认值为 0 。

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

切片初始化

  1. 声明的同时初始化
// 完整写法
var s []int = []int{1,2,3}
// 简化写法
s := []int{1, 2, 3}   

直接完成了声明和初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3。

  1. 用数组初始化切片

语法:

sliceName := arrayName[startIndex:endIndex]
  • startIndex:切片起始索引,默认表示从 arr 的第一个元素开始
  • endIndex:切片结束索引(不包括),默认表示一直到 arr 的最后一个元素 startIndex 和 endIndex 不设置则使用默认值。如s := arr[:]此时 s 的初始值即为数组 arr 的值。

在使用数组初始化切片时,注意切片不可以越界

var arr = [5]int{1,2,3,4,5}
s := [2:4]
fmt.Println(arr)    //  [1,2,3,4,5]
fmt.Println(s)  //  [3,4]

假如我们修改了切片中的数据,数组中的数据会不会发生改变呢? 继续使用上方代码进行测试:

s[0] = 6
fmt.Println(arr)    //  [1,2,6,4,5]
fmt.Println(s)  //  [6,4]

通过测试,可以发现修改切片中值,不仅切片会发生改变,数组中对应下标的值也会发生改变,由此可见切片是一个引用类型。

len()和cap()

可以使用 len() 方法获取切片长度。

可以使用 cap() 方法计算切片容量,测量切片最长可以达到多少。

s := make([]int,3)
fmt.Println(s,len(s),cap(s))  //  [0,0,0] 3 3

切片的长度总是 <= 切片的容量

append()

Go语言的内建函数 append() 可以为切片动态添加元素,代码如下所示:

var s []int
fmt.Println(s,len(s),cap(s))    //  [] 0 0
s = append(s, 1) // 追加1个元素
fmt.Println(s,len(s),cap(s))    //  [1] 1 1
s = append(s, 1, 2, 3) // 追加多个元素, 手写解包方式
fmt.Println(s,len(s),cap(s))    //  [1 1 2 3] 4 4
s = append(s, []int{1,2,3}...) // 追加一个切片, 切片需要解包
fmt.Println(s,len(s),cap(s))    //  [1 1 2 3 1 2 3] 7 8

在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。 通过上方代码,可以发现切片在扩容时容量是根据 2 的倍数进行扩容。1,2,4,8,16,32......如果查看扩容后切片的地址就会发现,每次扩容(也就是cap()的值发生变化)切片指向的内存地址就会发生改变。

var s []int
fmt.Printf("%p\n",s)    //  0x0
s = append(s, 1) // 
fmt.Printf("%p\n",s)    //  0xc0000140f8
s = append(s, 1, 2, 3) // 
fmt.Printf("%p\n",s)    //  0xc00001e0e0
s = append(s, []int{1,2,3}...) 
fmt.Printf("%p\n",s)    //  0xc00001c180

假如将 append() 函数扩容后的切片赋值给一个新的变量,原切片的值并不会发生改变。

s := []int{1,2}
s1 := append(s,3)
fmt.Println(s,s1)   // [1 2] [1 2 3]

append() 不仅可以在尾部添加元素,还可以在首部添加元素,代码示例如下:

s := []int{1,2}
s = append([]int{0},s...)
fmt.Println(s)  // [0 1 2]

第一个参数必须为一个切片,不可以使用 s = append(1,s) 方式。

在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

copy()

Go语言的内置函数 copy() 可以将一个切片复制到另一个切片中。copy 第一个参数为目标切片,第二个参数为源切片,会返回一个值表示从原切片src复制到目的切片的长度。 当目标切片的长度 > 源切片的长度,代码如下所示:

s1 := []int{1,2,3}
s2 := []int{4,5,6,7}
copy(s2,s1) //  将 s1 中的值赋值给 s2
fmt.Println(s1,s2)  //  [1 2 3] [1 2 3 7]

当目标切片的长度 < 源切片的长度,代码如下所示:

s1 := []int{1,2,3}
s2 := []int{4,5,6,7}
copy(s1,s2) //  将 s2 中的值赋值给 s1
fmt.Println(s1,s2)  //  [4 5 6] [4 5 6 7]

使用 copy 函数实现的切片拷贝是深拷贝,源切片和目的切片各自都有彼此独立的底层数组空间,各自的修改,彼此不受影响。

浅拷贝则是源切片和目的切片共享同一底层数组空间,源切片修改,目的切片同样被修改。可以通过赋值符号实现,代码如下所示:

s1 := []int{1,2,3}
s3 := s1
fmt.Printf("s1的地址:%p,s2的地址:%p,s1 = %v,s2 = %v",s1,s2,s1,s2)  //  s1的地址:0xc000132000,s2的地址:0xc000132000,s1 = [1 2 3],s2 = [1 2 3]

切片遍历

  1. 普通for循环
s := []int{1,2,3,4}
for i := 0 ; i < len(s) ; i++{
    fmt.Println(s[i])
}
  1. for-range 遍历
for index,value := range s{
    fmt.Println(index,value)
}

输出结果:

0 1
1 2
2 3
3 4

如果不想使用索引值,可以使用 _ 替代 index。for _,value := range s