1.概述
自动扩容的数组。扩容操作,主要体现在 append() 上。
2.类型
相对于数组,切片可以看做是容量可变(长度可变)的数组。当切片预先开辟的存储空
间已满时,可以通过扩容的方案,来增加存储空间,进而存储更多的元素。
类型的定义: []T
语法上,就是没有长度限制的数组!T 可以是任意类型
代码示例:
func main() {
var sli []int
fmt.Printf("%T/n", sli) // []int
fmt.Println(sli) // []
}
3.字面量
字面量支持如下:
- []int{1,2, 3, 4},列出全部元素
- []int{0:10, 5:60},通过索引指定部分元素 通过字面量语法,通过类型推导,得到切片类型。
代码示例:
func main() {
s1 := []int{1,2,3,4}
s2 := []int{1:3, 5:3}
fmt.Println(s1) // [1 2 3 4]
fmt.Println(s2) // [0 3 0 0 0 3]
}
4.长度和容量,len(), cap()
对于切片来说,存在两个概念:
- 长度,使用len()内置函数获取,表示切片中元素的个数。
- 容量,使用cap()内置函数获取,表示切片可以支持的最多的元素个数。
其中,容量可以在使用 make 创造切片是指定,或者在为切片增加元素时通过扩容来改变。
5.make() 创建切片
语法:make(类型,长度,容量)
代码示例:
func main() {
s1 := make([]int, 5, 10)
fmt.Printf("%T\n", s1) // []int
fmt.Println(s1, len(s1), cap(s1)) // [0 0 0 0 0] 5 10
}
如上构建的切片,有 10 个容量,意味着最多可以存储 10 个元素。
但当前仅仅有 5 个真实元素,就是说长度为 5。意味着最大的索引(5-1)为 4。
作用:
使用 make 构建切片的优势在于,当明确切片的容量时,可以通过 make 预先在 内存中开辟空间,避免了元素个数增加,频繁扩容的内存操作。
6.append(),追加元素
当需要在当前切片中,追加(增加)新的元素时,需要使用 append()内置函数来实现。
func main() {
s1 := make([]int, 5, 10)
fmt.Printf("%T\n", s1) // []int
fmt.Println(s1, len(s1), cap(s1)) // [0 0 0 0 0] 5 10
s2 := append(s1, 1024)
fmt.Println(s1, len(s1), cap(s1)) // [0 0 0 0 0] 5 10
fmt.Println(s2, len(s2), cap(s2)) // [0 0 0 0 0 1024] 6 10
}
通过结果分析,append 的返回值,是新增了元素的切片,由于原切片容量足以加入新的元素,导致新切片的容量不会改变,但是长度改变了,增加一个有效元素!
若当前容量已经用尽,len() == cap(), 此时,再次追加 append,会导致切片扩容,演示:
func main() {
s1 := make([]int, 5, 10)
fmt.Printf("%T\n", s1) // []int
fmt.Println(s1, len(s1), cap(s1)) // [0 0 0 0 0] 5 10
s3 := append(s1, 1,2,3,4,5,6)
fmt.Println(s3, len(s3), cap(s3))
//输出 [0 0 0 0 0 1 2 3 4 5 6] 11 20
}
注意:扩容,不是增加几个元素,容量就增加几个。 内部会有一定的扩容算法。
大概: 当容量较小时,大概率会翻倍增加。 当容量较大时,会增加一定的数量,不是翻倍了。
7.引用类型
也叫地址类型,意味着当得到一个切片类型的变量时,该变量存储的不是切片,而是切 片值的地址,导致使用切片进行赋值传递时,多个切片变量,会同时使用同一个切片的值。
如下所示:
func main() {
s1 := []int{1,2,3}
s2 := s1
fmt.Println(s1, s2) // [1 2 3] [1 2 3]
s2[0] = 2
fmt.Println(s1, s2) // [2 2 3] [2 2 3]
}
大概描述图:
图例中,对切片类型存储的结构圆圈,仅仅描述了地址部分,整体结构会更复杂。
8.拷贝操作,copy()
由于引用类型间的复制,不会得到新的数据,因此若需要得到原始数据的拷贝,使用copy()内置函数来实现。
语法:copy(目标切片,原始切片)
将元素从原始切片,拷贝到目标切片。原始切片和目标切片不是同一个切片。
代码演示
s1 := []int{1, 2, 3}
s2 := make([]int, 3)
copy(s2, s1)
fmt.Println(s1, s2) // [1 2 3] [1 2 3]
s2[1] = 22
fmt.Println(s1, s2) // [1 2 3] [1 22 3]
说明:
copy 的流程,就是遍历 s1(原始切片),利用索引的对应关系,依次为 s2(目标切片)赋值。 语法上要求,s2 的容量要大于或等于 s1。
9.切片结构
结构图:
如图所示,切片由,底层数组和切片上层结构组成。 底层数组存储元素数据,上层结构,记录了数组的地址,以及该切片的长度和容量。
当切片需要扩容时,意味着需要更换底层数组。得到一个更大长度的底层数组,就是切 片的扩容。
10.操作
1) 索引操作 [] : 与数组一致
2)长度 len()
3)容量 cap()
4)追加元素 append()
5)拷贝,copy()
6)基于数组做切片
语法 :
arr[start:end]
- start, 开始索引,包含,省略表示 0。
- end, 结束索引,不包含,省略表示到最后
代码演示:
func main() {
arr := [...]int{1,2,3,4,5,6}
s1 := arr[2:5]
fmt.Println(s1, len(s1), cap(s1)) //[3 4 5] 3 4
s2 := arr[:3]
fmt.Println(s2, len(s2), cap(s2)) //[1 2 3] 3 6
s3 := arr[3:]
fmt.Println(s3, len(s3), cap(s3)) //[4 5 6] 3 3
s4 := arr[:]
fmt.Println(s4, len(s4), cap(s4)) //[1 2 3 4 5 6] 6 6
s4[2] = 1024
fmt.Println(arr, s1, s2, s3, s4)
// [1 2 1024 4 5 6] [1024 4 5] [1 2 1024] [4 5 6] [1 2 1024 4 5 6]
}
注意:
①得到的切片,元素个数决定长度。容量是从开始元素到数组最后一个元素的数量, 作为切片的容量。
②得到的切片,所使用的底层数组为同一个。修改了任意的元素,都会对底层数组和对应 的切片带来影响。(也是切片的结构导致的)。如下图:
7)基于切片做切片
语法与基于数组做切片是一致的。 原理也是一致的。
代码演示:
func main() {
s0 := []int{1,2,3,4,5,6}
s1 := s0[2:5]
fmt.Println(s1, len(s1), cap(s1)) //[3 4 5] 3 4
s2 := s0[:3]
fmt.Println(s2, len(s2), cap(s2)) //[1 2 3] 3 6
s3 := s0[3:]
fmt.Println(s3, len(s3), cap(s3)) //[4 5 6] 3 3
s4 := s0[:]
fmt.Println(s4, len(s4), cap(s4)) //[1 2 3 4 5 6] 6 6
s4[2] = 1024
fmt.Println(s0, s1, s2, s3, s4)
// [1 2 1024 4 5 6] [1024 4 5] [1 2 1024] [4 5 6] [1 2 1024 4 5 6]
}
容量也同基于数组的切片
全部的切片也是同一个底层数组,修改一个,也会影响相关的元素。
8)... 展开切片
场景:当需要将某个切片内的元素,全部追加到另一个切片中时,语法如下:
func main() {
s1 := []int{1,2,3}
s2 := []int{4,5,6,7,8}
s3 := append(s1, s2...)
fmt.Println(s3) // [1 2 3 4 5 6 7 8]
}
s2... 就是使用 ... 展开运算符,将 s2 展开后,作为参数传递到 append 中。
9)遍历
与遍历数组是一致的。使用 for range 结构。
- 得到索引和值
for i, v := range slice {
}
- 仅仅得到索引
for i := range slice {
}
- 仅仅得到值
for _, v := range slice {
}
10)nil 与 切片
nil,空指针,指针类型的零值。
由于切片为引用类型,当切片使用 var 定义同时未指定初始值时,分配的零值就是 nil。
可以比较 与 nil 的关系,结果为真,相等。
代码:
var s []int
fmt.Println(s, s == nil) // [] true
注意:
var s []T 与 s:=[]T{} 是不同的。
var s []T 定义的切片,使用 nil 进行零值初始化的切片,使用切片的 s 的零值 nil 初始
化。这种定义语法 意义不大。
s := []T{}, 使用字面量定义的切片,使用元素的零值进行初始化,字面量值的语法表示。
意味着已经存在这个值[]T{}在内存空间中,不再是空指针了。
代码示例:
var s []int
fmt.Println(s, s == nil) // [] true
s1 := []int{}
fmt.Println(s1, s1 == nil) // [] false
11.练习题
1)利用切片,模拟队列结构
队列结构,先进先出结构。 支持两个操作:
- 从左边加入元素
-
从右边取出元素
提示:提示,使用 append(), len(),[:]配合实现。
答案:
// 队列
q := []int{2, 3} // 入
ele := 1
q = append([]int{ele}, q...)
fmt.Println(q) // [1 2 3]
// 出
ele = q[len(q)-1]
q = q[:len(q)-1]
fmt.Println(ele, q) // 3 [1 2]
2)利用切片,模拟栈结构
栈结构,先进后出。 支持两个操作:
- 从左边加入元素
-
从左边取出元素
答案:
// 栈
q := []int{2, 3}
// 入
ele := 1
q = append([]int{ele}, q...)
fmt.Println(q) // [1 2 3]
// 出
ele = q[0] q = q[1:]
fmt.Println(ele, q) // 1 [2 3]
3)删除指定索引的元素
给出指定的索引,将该索引删除,得到新的切片, 例如,给出 索引为 3.
答案:
// 删除元素
s := []int{1, 2, 3, 4, 5, 6}
idx := 3
s = append(s[:idx], s[idx+1:]...)
fmt.Println(s) // [1 2 3 5 6]
4)在指定的索引位置,插入某些元素
给出索引位置 3, 插入 10, 11, 演示:
答案:
// 插入元素
s := []int{1, 2, 3, 4, 5, 6}
inserts := []int{10, 11}
idx := 3
rest := make([]int, len(s)-idx)
copy(rest, s[idx:])
s = append(append(s[:idx], inserts...), rest...)
fmt.Println(s) // [1 2 3 10 11 4 5 6]