Golang学习笔记-06切片

切片的本质

数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,比如不能动态添加元素。

Go中提供了一种灵活,功能强悍的内置类型 切片slice (“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片Slice是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装,它非常灵活,支持自动扩容。切片本身没有任何数据。它们只是对现有数组的引用。与数组相比,切片不需要设定长度,即声明时在[ ]中不用设定值。

slice从底层来说,其实就是一个数据结构(struct结构体)。可以看出,切片是对数组的抽象。

1
2
3
4
5
type slice struct{
ptr *[]int //指针,指向数组中slice指定的开始位置
len int //长度,随元素数量的变化而变化
cap int //容量,也就是slice开始位置到数组的最后位置的长度,最大长度(可扩容的)
}

注意,切片是引用类型,数组是值类型

切片的定义和初始化

声明语法

1
var 变量名 []类型

例如

1
2
var a []string              //声明一个字符串切片
fmt.Println(a) //输出 []

初始化

1
2
3
4
5
6
7
8
9
10
var b = []int{}             //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
// fmt.Println(c == d) //这样比较是错误的,切片是引用类型,不支持直接比较,只能和nil比较

注意:

1
2
var b = [...]int{123}	//这是声明一个数组,并使用编译器自动推断数组的长度
var b = []int{123} //这才是声明一个切片

切片表达式

切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。

简单切片表达式

切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。 切片表达式中的lowhigh表示一个索引范围(左包含,又不包含),也就是下面代码中从数组a中选出1<=索引值<4的元素组成切片s,得到的切片长度=high-low,容量等于得到的切片的底层数组的容量。

根据缺省条件可分为下面几种:

将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片(左闭右开),长度为 high-low

1
var s = arr[low:high]

例如

1
2
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]

缺省endIndex时将表示一直到arr的最后一个元素

1
s := arr[low:]

缺省startIndex时将表示从arr的第一个元素开始

1
s := arr[:high]

全部缺省,表示直接从该数组的全部元素构建

1
s := arr[:]

注意:对于数组或字符串,必须保证索引不越界

对切片再执行切片表达式时(切片再切片),high的上限边界是切片的容量cap(a),而不是长度。常量索引必须是非负的,并且可以用int类型的值表示。对于数组或常量字符串,常量索引也必须在有效范围内。如果lowhigh两个指标都是常数,它们必须满足low <= high。如果索引在运行时超出范围,就会发生运行时panic

完整切片表达式

对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:

1
a[low : high : max]

上面的代码会构造与简单切片表达式a[low: high]相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low。在完整切片表达式中只有第一个索引值(low)可以省略,但是冒号:不能省略,它默认为0。

1
2
3
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))

输出结果:

1
t:[2 3] len(t):2 cap(t):4

完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。

动态定义

上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:

make是内建函数,你可以在 http://docscn.studygolang.com/pkg/builtin/#make 这儿看到它,简单的说就是为slice、map和channel分配内存和初始化,返回一个引用类型的对象

1
make([]T, len, cap) //T:切片的元素类型,size:切片中元素的数量,cap:切片的容量

当然,你也可以不指定cap,这是cap默认等于len。返回的切片的数组元素全部为零值

切片的访问和修改

同数组一样,切片也可以通过下标来访问对应的元素,并修改它。

slice没有自己的任何数据。它只是底层数组的一个表示。对slice所做的任何修改都将反映在底层数组中。

也就是说,如果一个切片是根据一个数组创建的,那么如果我们对这个切片进行修改,其对应的底层数组也会改变

1
2
3
4
5
6
7
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)

输出

1
2
array before [57 89 90 82 100 78 67 69 59]  
array after [57 89 91 83 101 78 67 69 59]

先添加元素后再修改

1
2
3
4
5
6
7
8
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
dslice=append(dslice, 1000)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)

输出

1
2
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 1001 67 69 59]

当多个片共享相同的底层数组时,每个元素所做的更改将在数组中反映出来。

1
2
3
4
5
6
7
8
numa := [3]int{78, 79 ,80}//创建一个数组
nums1 := numa[:] //根据数组创建一个切片
nums2 := numa[:]
fmt.Println("array before change 1",numa)
nums1[0] = 100
fmt.Println("array after modification to slice nums1", numa)
nums2[1] = 101
fmt.Println("array after modification to slice nums2", numa)

结果

1
2
3
array before change 1 [78 79 80]  
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]

切片的赋值拷贝

1
2
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组

对一个切片的修改会影响另一个切片的内容,如果不想有该特性,可以使用内建的copy函数,具体可见下文

切片的长度和容量

切片的长度是切片中元素的数量。切片的容量是从创建切片的索引开始的底层数组中元素的数量。

长度

切片的长度可以通过内建函数 len() 函数来获取

容量

切片的容量可以通过内建函数 cap() 函数来获取

注意,切片的长度是可以随着切片的增删来改变的。切片的容量也不是固定的,当我们在向切片slice append添加数据的时候,Golang 会检查底层的数组的长度是否已经不够,如果长度不够,Golang 则会新建一个数组,把原数组的数据拷贝过去,再将 slice 中的指向数组的指针指向新的数组,也就是扩容,这时候容量cap也会随之改变。

切片的遍历

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。

1
2
3
4
5
6
7
8
s := []int{1, 3, 5}
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}

for index, value := range s {
fmt.Println(index, value)
}

空切片

一个切片在未初始化之前默认为 nil,长度为 0

要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。

切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:

1
2
3
var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。

nil是指底层数组没有分配内存空间,len=0是指分配了空间但是没有填充数据元素。

切片的添加和复制

append添加

Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。

1
2
3
4
5
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]

注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。

1
2
var s []int
s = append(s, 1, 2, 3)

切片的扩容

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

关于切片扩容的具体详情可以看这里 https://h3l.github.io/posts/slice-append-grow-analyze/

总结一下:slice 在 cap 长度小于 1024 之前容量是翻倍增长,在 cap 长度大于 1024 之后,因为存在着内存对齐,slice 的容量增长方式是最小增加 25%。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

copy复制

在上文的 切片的访问和修改 中提到过:当多个片共享相同的底层数组时,每个元素所做的更改将在数组中反映出来。

由于切片是引用类型,所以nums1和nums2其实都指向了同一块内存地址(数组numa)。修改nums2的同时nums1的值也会发生变化。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

1
copy(destSlice, srcSlice []T)

其中:

  • srcSlice: 数据来源切片
  • destSlice: 目标切片

这样目标切片修改后源切片并不会受到影响

切片的删除

go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:

1
2
3
4
5
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

文章作者: Oxywen
文章链接: https://oxywen.cn/post/go/7/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 不闻星河须臾