4.2 切片
在Go程序中,切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++语言中的数组类型,或者 Python语言中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。
4.2.1 声明、初始化切片
在 Go 语言中,有如下三种方式来声明和初始化一个切片。
(1)直接声明:使用空的中括号[]来声明一个切片变量,然后可以通过 append 函数或赋值语句来添加元素。例如:
var s []int // 声明一个空的切片
s = append(s, 1, 2, 3) // 添加元素
(2)使用make()函数:通过make()函数可以创建一个指定长度和容量的切片,例如:
s := make([]int, 3, 5) // 创建一个长度为3,容量为5的切片
(3)通过数组创建:可以通过一个数组来创建切片,使用切片表达式来指定切片的范围。例如:
a := [5]int{1, 2, 3, 4, 5} // 声明一个数组
s := a[1:3] // 使用数组创建一个切片,包含 a[1] 和 a[2] 两个元素
在上述演示代码中,切片表达式a[1:3]表示从数组a中索引为1到2的元素,左闭右开。所以最终得到的切片s包含a[1]和a[2]两个元素,即[2, 3]。
无论使用哪种方式,最终都会得到一个切片变量,可以通过索引来访问和修改切片中的元素,也可以使用内置函数对切片进行操作,例如append()、copy()、len()和cap()等。
注意:切片和数组的区别
在 Go 语言中,数组和切片都是用于存储一系列相同类型的值的数据结构。但它们之间有一些重要的区别:
- 长度和容量的差异:数组的长度是固定的,而切片的长度可以动态改变,它的容量是底层数组的长度,可以通过内置函数cap获取。
- 内存使用的差异:数组在声明时会分配固定大小的连续内存空间,而切片只是一个引用类型,底层指向一个连续的内存空间,当切片扩容时,会重新分配更大的内存并将原有数据复制到新的内存中。
- 传递方式的差异:数组在函数间传递时会进行值拷贝,而切片则是传递引用,多个切片变量可能会共享同一个底层数组。
- 初始化方式的差异:数组的长度必须在声明时指定,而切片可以通过make函数或者简短声明的方式来动态创建。
总的来说,切片比数组更加灵活,更适合动态场景下的数据操作,而数组则更适用于需要固定大小的数据集合。在实际开发中,根据具体需求来选择数组或者切片。
实例4-3:创建和使用切片(源码路径:Go-codes\4\qie.go)
实例文件qie.go的具体实现代码如下所示。
package main
import "fmt"
func main() {
// 声明一个切片
var s []int
fmt.Println(s)
// 使用 make 函数创建一个长度为 3,容量为 5 的切片
s = make([]int, 3, 5)
fmt.Println(s)
// 使用短变量声明语法创建一个切片
s1 := []int{1, 2, 3}
fmt.Println(s1)
// 从切片中取出一个子切片
s2 := s1[1:3]
fmt.Println(s2)
// 向切片中添加元素
s2 = append(s2, 4, 5)
fmt.Println(s2)
}
- 首先声明了一个切片s,输出它的值时会显示为空切片[]。接着,我们使用make函数创建了一个长度为3,容量为5的切片s,并输出它的值[0 0 0]。
- 然后,使用短变量声明语法创建了一个切片s1,并输出它的值[1 2 3]。接着,我们从s1中取出一个子切片s2,它包含了s1的第二个和第三个元素,并输出它的值[2 3]。
- 最后,使用append函数向切片s2中添加了两个元素4和5,并输出它的值[2 3 4 5]。由于切片s2和切片s1共享同一个底层数组,因此s1的值也会受到影响,输出[1 2 3 4 5]。
执行后会输出:
[]
[0 0 0]
[1 2 3]
[2 3]
[2 3 4 5]
4.2.2 向切片中添加元素
当需要向一个切片添加元素时,可以使用 Go 语言中的内置函数 append()。请看下面的代码(源码路径:Go-codes\4\addqie.go),向一个切片中添加了三个元素。
package main
import "fmt"
func main() {
// 声明一个空的整型切片
var numbers []int
// 向切片中添加三个元素
numbers = append(numbers, 1)
numbers = append(numbers, 2)
numbers = append(numbers, 3)
// 打印切片中的元素
fmt.Println(numbers)
}
在上述代码中,首先声明了一个空的整型切片 numbers,然后使用 append() 函数向切片中添加了三个元素,即 1、2 和 3。最后,打印输出整个切片执行后会输出:
[1 2 3]
注意:添加切片元素的通俗理解
往一个切片中不断添加元素的过程,类似于公司搬家,公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工,随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变,因此公司只能选择搬家,每次搬家就需要将所有的人员转移到新的办公点。
- 员工和工位就是切片中的元素。
- 办公地就是分配好的内存。
- 搬家就是重新分配内存。
- 无论搬多少次家,公司名称始终不会变,代表外部使用切片的变量名不会修改。
- 由于搬家后地址发生变化,因此内存“地址”也会有修改。
4.2.3 函数len()和cap()
Go语言中的切片是一个引用类型,它包含了一个指向底层数组的指针、长度和容量三个属性。其中,函数len()用于获取切片的长度,也就是切片中元素的数量,而函数cap()用于获取切片的容量,也就是底层数组中元素的数量。具体说明如下:
- 函数len()返回的是切片中元素的数量,即切片的长度。例如,对于一个长度为5的切片,调用len()函数会返回5。如果切片是空的,则len()函数返回0。
- 函数cap()返回的是切片的容量,即底层数组中元素的数量。底层数组是切片的一个重要属性,它是切片所引用的数组。切片的容量可以通过调整切片的大小来改变,但不能超过底层数组的容量。例如,如果底层数组的长度为10,但是切片的容量只有5,那么在切片中只能存储5个元素,超过5个元素的话就会导致运行时错误。
注意:切片的长度和容量可能是不同的。切片的长度是指切片中实际存储的元素数量,而容量是指底层数组中可以存储的元素数量。当切片长度等于容量时,表示切片已经占满了底层数组的空间,再添加元素就会导致切片扩容。
当我们定义一个切片时,可以使用len()和cap()函数来获取它的长度和容量,例如下面的演示代码:
s := []int{1, 2, 3, 4, 5} // 定义一个切片
fmt.Println(len(s)) // 输出:5
fmt.Println(cap(s)) // 输出:5
在使用append()函数添加元素时,需要注意切片的容量是否已经达到了极限,例如下面的演示代码:
s := make([]int, 0, 5) // 定义一个长度为0,容量为5的切片
fmt.Println(len(s)) // 输出:0
fmt.Println(cap(s)) // 输出:5
s = append(s, 1, 2, 3, 4, 5) // 添加5个元素到切片中
fmt.Println(len(s)) // 输出:5
fmt.Println(cap(s)) // 输出:5
s = append(s, 6) // 添加一个元素到切片中
fmt.Println(len(s)) // 输出:6
fmt.Println(cap(s)) // 输出:10,此时切片已经扩容了
另外,切片可以使用切片表达式获取子切片,也可以使用len()和cap()函数来获取子切片的长度和容量,例如下面的演示代码:
s := []int{1, 2, 3, 4, 5}
s1 := s[1:3] // 获取s[1]到s[2]的子切片
fmt.Println(len(s1)) // 输出:2
fmt.Println(cap(s1)) // 输出:4,底层数组中还有4个元素可以使用
4.2.4 切片复制(切片拷贝)
在Go语言中,内置函数copy()可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。使用函数copy()的语法格式如下:
copy( destSlice, srcSlice []T) int
在上述格式中,“srcSlice”为数据来源切片,“destSlice”为复制的目标(也就是将“srcSlice”复制到“destSlice”),目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致,函数copy()的返回值表示实际发生复制的元素个数。
例如下面的代码展示了使用函数copy()将一个切片复制到另一个切片的过程:
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
虽然通过循环复制切片元素更直接,不过使用内置的函数copy()的方式更加方便,函数copy()的第一个参数是要复制的目标 slice,第二个参数是源 slice,两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。
请看下面的演示代码(源码路径:Go-codes\4\copyqie.go),首先声明了一个整型切片 source,包含了五个元素。接着,使用 make() 函数声明了另一个整型切片 destination,其长度与 source 切片相同。然后,使用 copy() 函数将 source 切片中的元素复制到 destination 切片中。最后,打印输出切片destination中的元素。
func main() {
// 声明两个整型切片
source := []int{1, 2, 3, 4, 5}
destination := make([]int, len(source))
// 使用 copy() 函数将 source 切片中的元素复制到 destination 切片中
copy(destination, source)
// 打印输出 destination 切片
fmt.Println(destination)
}
执行后会输出:
[1 2 3 4 5]
下面是一个稍微复杂一些的使用函数copy()的例子(源码路径:Go-codes\4\copyfuza.go),假设有两个切片 src 和 dst,分别存储了一些数据,现在要求将 src 中的部分元素复制到 dst 中指定的位置。
package main
import "fmt"
func main() {
// 定义源切片
src := []int{1, 2, 3, 4, 5}
// 定义目标切片
dst := make([]int, 8)
// 将源切片的第 2、3、4 个元素复制到目标切片的第 3、4、5 个位置
copy(dst[3:6], src[1:4])
// 打印结果
fmt.Println(dst)
}
执行后会输出下面的内容,可以看到,copy() 函数将 src 切片的第 2、3、4 个元素复制到了 dst 切片的第 3、4、5 个位置。其中,dst 切片的前三个元素和后两个元素都是 0,因为我们之前虽然使用函数make()创建了长度为 8 的 dst 切片,但是并没有初始化其中的元素。
[0 0 0 2 3 4 0 0]
4.2.5 从切片中删除元素
在Go语言中并没有提供删除切片元素的专用的语法或接口,我们需要使用切片本身的特性来删除元素。根据要删除元素的位置有四种情况,分别是从开头位置删除、从中间位置删除、从尾部删除和指定位置删除,其中删除切片尾部的元素的速度最快。
(1)从开头位置删除
我们可以用函数append()原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)从开头位置删除元素:
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
还可以用函数copy()来删除开头的元素:
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
(2)从中间位置删除
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用函数append()或函数copy()原地完成:
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
(3)从尾部删除
尾部删除比较简单,请看下面的代码:
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况
(4)指定位置删除
要删除切片中的指定位置元素,可以使用Go语言的内置函数append()实现。例如下面的演示代码:
// 假设有一个切片 s,要删除其中的第 i 个元素
i := 2
s := []int{1, 2, 3, 4, 5}
// 删除第 i 个元素
s = append(s[:i], s[i+1:]...)
在这个例子中,我们定义了一个切片 s 和要删除的元素的下标 i。然后,我们使用 append() 函数和切片的切片操作来删除第 i 个元素。具体来说,s[:i] 表示从切片 s 的第 0 个元素到第 i-1 个元素组成的切片,s[i+1:] 表示从切片 s 的第 i+1 个元素到最后一个元素组成的切片。将这两个切片使用 ... 操作符展开,然后使用 append() 函数将它们连接起来,就得到了一个不包含第 i 个元素的新切片。最后,将新切片赋值给原来的切片 s,就完成了元素的删除操作。
4.2.6 遍历切片中的元素
在Go语言中,可以使用for循环和函数range()来遍历切片中的元素。例如,假设有一个切片mySlice,我们想遍历切片中的每个元素,可以使用以下代码实现。
mySlice := []int{1, 2, 3, 4, 5}
for i := 0; i < len(mySlice); i++ {
fmt.Println(mySlice[i])
}
在上面的代码中,for循环用于遍历切片中的每个元素。
另外,也可以使用函数range()遍历切片中的元素,例如下面的演示代码。
mySlice := []int{1, 2, 3, 4, 5}
for index, value := range mySlice {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
在上面的代码中,函数range()用于返回切片中每个元素的索引和值,然后使用for循环遍历这些值并将它们打印出来。注意,我们可以选择忽略索引或值中的任何一个,例如下面的演示代码。
mySlice := []int{1, 2, 3, 4, 5}
for _, value := range mySlice {
fmt.Println(value)
}
在上面的代码中,我们使用下划线“_”来忽略索引值,仅遍历切片中的值并将其打印出来。
4.2.7 多维切片
在 Go 语言中,多维切片本质上是切片的切片。声明一个多维数组的语法格式如下:
var sliceName [][]...[]sliceType
其中,sliceName 为切片的名字,sliceType为切片的类型,每个[ ]代表着一个维度,切片有几个维度就需要几个[ ]。
例如下面的代码声明了一个二维切片,其中,第一对方括号[]表示这是一个切片类型,第二对方括号[]表示切片元素也是切片类型。
slice2D := [][]int{
[]int{1, 2},
[]int{3, 4},
}
可以通过如下方式访问二维切片中的元素:
slice2D[0][0] // 访问第一行第一列
slice2D[1][1] // 访问第二行第二列
类似地,可以定义更高维度的切片。例如下面的代码定义了一个三维切片:
slice3D := [][][]int{
[][]int{
[]int{1, 2},
[]int{3, 4},
},
[][]int{
[]int{5, 6},
[]int{7, 8},
},
}
访问三维切片中的元素的方式与访问二维切片类似,只是需要多一层索引。例如下面的代码:
slice3D[0][0][0] // 访问第一层第一行第一列
slice3D[1][1][1] // 访问第二层第二行第二列
注意:由于在 Go 语言中切片是引用类型,因此当将一个切片赋值给另一个变量时,它们实际上是共享同一个底层数组的。这意味着,如果修改其中一个切片的元素,另一个切片中的相应元素也会发生变化。这一点在多维切片中尤为重要,需要特别注意。
实例4-4:创建并访问二维切片(源码路径:Go-codes\4\duoqie.go)
实例文件duoqie.go的具体实现代码如下所示。
package main
import "fmt"
func main() {
// 创建一个二维切片,其中有 2 行和 3 列
slice2D := make([][]int, 2)
for i := range slice2D {
slice2D[i] = make([]int, 3)
}
// 给二维切片赋值
slice2D[0][0] = 1
slice2D[0][1] = 2
slice2D[0][2] = 3
slice2D[1][0] = 4
slice2D[1][1] = 5
slice2D[1][2] = 6
// 访问二维切片
fmt.Println(slice2D[0][0]) // 输出 1
fmt.Println(slice2D[1][2]) // 输出 6
}
在上述代码中,我们使用函数make()创建了一个二维切片,并为其分配了 2 行和 3 列的空间。然后,我们使用下标操作符来访问和修改切片中的元素。执行后会输出:
1
6
下面是一个遍历输出二维维切片元素的例子(源码路径:Go-codes\4\bduoqie.go),使用嵌套循环来遍历输出了切片中的每一个元素。
package main
import "fmt"
func main() {
slice := [][]int{{1, 2}, {3, 4}, {5, 6}}
for i := 0; i < len(slice); i++ {
for j := 0; j < len(slice[i]); j++ {
fmt.Printf("%d ", slice[i][j])
}
fmt.Println()
}
}
在上述代码中,我们定义了一个二维整数切片 slice,其中包含三个长度为2的子切片。然后我们使用两个嵌套的 for 循环遍历 slice 中的每一个元素,并使用 fmt.Printf() 函数输出每个元素的值。最内层的循环遍历每个子切片中的元素,而最外层的循环遍历所有子切片。输出结果如下:
1 2
3 4
5 6