介绍
在前一节中,我们讨论了 Go 语言中的数组。然而,数组有一些局限性:一旦声明并初始化后,其长度无法改变。因此,数组在日常编程中并不常用。相比之下,切片(slice)更为常见,它提供了更灵活的数据结构。
知识点:
- 定义切片
- 初始化切片
- 切片的操作,即增、删、改、查
- 扩展切片
- 切片截取
- 多维切片
在前一节中,我们讨论了 Go 语言中的数组。然而,数组有一些局限性:一旦声明并初始化后,其长度无法改变。因此,数组在日常编程中并不常用。相比之下,切片(slice)更为常见,它提供了更灵活的数据结构。
知识点:
切片(Slice)与数组类似,它们都是用于存储相同数据类型元素的容器。然而,数组有一些局限性:一旦声明并初始化后,其长度无法改变。尽管数组在某些场景下有用,但它们不够灵活。因此,切片在日常编程中更为常用。
在 Go 语言中,切片是通过数组实现的。切片本质上是一个可以动态改变长度的数组。我们可以对切片进行添加、删除、修改和查找元素等操作,而这些操作在数组中是无法实现的。
切片的初始化语法与数组非常相似。主要区别在于不需要指定元素的长度。让我们来看以下代码:
// 声明一个长度为 5 的数组
var a1 [5]byte
// 声明一个切片
var s1 []byte
如果我们声明一个 int
类型的数组,数组的每个元素的零值将是 0。
然而,如果我们声明一个切片,切片的零值将是 nil
。让我们创建一个名为 slice.go
的文件来验证这一点:
touch ~/project/slice.go
输入以下代码:
package main
import "fmt"
func main() {
var a [3]int
var s []int
fmt.Println(a[0] == 0) // true
fmt.Println(s == nil) // true
}
go run slice.go
运行代码后,将显示以下输出:
true
true
我们创建了一个数组 a
和一个切片 s
。我们将数组 a
的第一个元素与零进行比较,并检查切片 s
是否为 nil
。
正如我们所看到的,当我们声明一个切片时,它的零值是 nil
。这是因为切片并不存储任何数据;它们只是引用数组。切片指向底层的数组结构。
切片是一种复合数据类型,也称为结构体(struct
)。它是由不同类型的字段组成的复合类型。切片的内部结构由三个元素组成:指针、长度和容量。
type slice struct {
elem *type
len int
cap int
}
如前所述,该结构体引用了底层的数组。elem
指针指向数组的第一个元素,而 type
是被引用数组元素的类型。
len
和 cap
分别表示切片的长度和容量。你可以使用 len()
和 cap()
函数来获取切片的长度和容量。
下图展示了切片引用了一个 int
类型的底层数组,其长度为 8,容量为 10:
当你定义一个新的切片时,elem
指针会被初始化为零值(即 nil
)。指针的概念将在后续实验中介绍。现在只需注意,指针指向某个值的内存地址。在上图中,elem
指针指向底层数组第一个元素的地址。
由于切片的底层结构是数组,我们可以提取数组的指定长度作为切片的引用。以下代码段展示了这一点:
package main
import "fmt"
func main() {
// 定义一个长度为 10 的整型数组
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 声明一个空切片
var s1 []int
fmt.Println("切片 s1 为空:", s1 == nil)
// 使用数组截取获取切片
s1 = a[1:5]
s2 := a[2:5]
s3 := a[:]
fmt.Println("切片 s1、s2 和 s3 为空:", s1 == nil, s2 == nil, s3 == nil)
fmt.Println("数组 a 中的元素:", a)
fmt.Println("切片 s1 中的元素:", s1)
fmt.Println("切片 s2 中的元素:", s2)
fmt.Println("切片 s3 中的元素:", s3)
}
输出结果如下:
切片 s1 为空: true
切片 s1、s2 和 s3 为空: false false false
数组 a 中的元素: [0 1 2 3 4 5 6 7 8 9]
切片 s1 中的元素: [1 2 3 4]
切片 s2 中的元素: [2 3 4]
切片 s3 中的元素: [0 1 2 3 4 5 6 7 8 9]
在这个程序中,我们首先声明并初始化了数组 a
,然后使用数组截取将数组的一部分赋值给空切片 s1
。通过这种方式,我们创建了一个新的切片。
s1[1:5]
表示从数组 a
的索引 1 到索引 5 创建一个切片,但不包括索引 5 的元素。
注意: 在编程语言中,第一个元素的索引是 0,而不是 1。同样,数组中的第二个元素的索引是 1。
我们使用 :=
运算符将截取的数组直接赋值给切片 s2
。s3
也是如此,但没有指定范围,因此它会截取数组的所有元素。
下图展示了截取的过程。注意,切片的绿色部分表示对蓝色数组的引用。换句话说,它们共享同一个底层数组,即 a
。
切片的截取语法如下:
[start:end]
start
和 end
都是可选参数。当我们想要获取数组的所有元素时,可以省略 start
和 end
参数。这在前面的程序中的 s3 := a[:]
中得到了展示。
如果我们想要获取某个索引之后的所有元素,可以省略 end
参数。例如,a1[3:]
将获取从索引 3 开始的所有元素。
要获取某个索引之前的所有元素,可以省略 start
参数。例如,a1[:4]
将提取从索引 0 到索引 4 的所有元素,但不包括索引 4 的元素。
除了从数组中提取切片外,我们还可以从现有的切片中提取新的切片。操作与数组相同。以下是一个简单的示例:
package main
import "fmt"
func main() {
// 定义一个长度为 10 的整型数组
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 创建初始切片 s1
var s1 []int
s1 = a[1:7]
fmt.Printf("切片 s1: %d\t长度: %d\t容量: %d\n", s1, len(s1), cap(s1))
// 从初始切片 s1 中提取新的切片 s2
s2 := s1[2:4]
fmt.Printf("切片 s1: %d\t长度: %d\t容量: %d\n", s2, len(s2), cap(s2))
}
输出结果如下:
切片 s1: [1 2 3 4 5 6] 长度: 6 容量: 9
切片 s1: [3 4] 长度: 2 容量: 7
在这个程序中,我们通过截取数组 a
得到了切片 s1
。s1
的范围是从索引 1 到索引 7。通过使用 :=
,我们从 s1
中提取了一个新的切片 s2
。由于 s1
的截取是连续的,新的切片 s2
也将是连续的。
我们注意到,切片的容量会随着截取而改变。规则如下:
如果我们截取一个容量为 c
的切片,s[i:j]
的长度将是 j-i
,容量将是 c-i
。
对于 s1
,底层数组是 a
,a[1:7]
的容量是 9(即 10-1)。
对于 s2
,底层数组与 s1
相同。由于上一步截取的部分容量为 9,s1[2:4]
的容量变为 7(即 9-2)。
由于切片不存储数据,而是引用数组,修改切片的值也会同时改变底层数组的值。让我们来演示这一点:
package main
import "fmt"
func main() {
// 定义一个长度为 10 的整型数组
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[2:5]
s2 := a[:]
fmt.Println("修改前: ")
fmt.Println("数组 a 中的元素: ", a)
fmt.Println("数组 a 中的元素: ", s1)
fmt.Println("数组 a 中的元素: ", s2)
// 将切片 s1 中索引 2 的值修改为 23
s1[2] = 23
fmt.Println("修改后: ")
fmt.Println("数组 a 中的元素: ", a)
fmt.Println("数组 a 中的元素: ", s1)
fmt.Println("数组 a 中的元素: ", s2)
}
输出结果如下:
修改前:
数组 a 中的元素: [0 1 2 3 4 5 6 7 8 9]
数组 a 中的元素: [2 3 4]
数组 a 中的元素: [0 1 2 3 4 5 6 7 8 9]
修改后:
数组 a 中的元素: [0 1 23 3 4 5 6 7 8 9]
数组 a 中的元素: [2 3 23]
数组 a 中的元素: [0 1 23 3 4 5 6 7 8 9]
在这个程序中,切片 s1
和 s2
都引用了数组 a
。当切片 s1
将索引 2 的值修改为 23 时,数组 a
和切片 s2
的值也会被更新。
我们可以看到,数组 a
中索引 2 的值被修改为 23,这导致切片 s2
中索引 4 的值也被修改。
这会在程序开发中导致难以调试的 bug。因此,在日常编程中,我们应尽量避免多个切片引用同一个底层数组。
在本节中,我们将介绍 append
函数,它用于向切片添加元素。语法如下:
func append(slice []Type, elems ...Type) []Type
第一个参数是切片 slice
,其余参数是要添加到切片中的元素。末尾的 []Type
表示 append
函数将返回一个与 slice
数据类型相同的新切片。
elems
后面的 ...
表示这是一个可变参数,意味着可以输入一个或多个参数。
以下是使用 append
函数的示例:
package main
import "fmt"
func main() {
// 定义一个长度为 10 的整型数组
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[1:7]
fmt.Printf("初始 s1 值: %d\t长度: %d\t容量: %d\n", s1, len(s1), cap(s1))
s1 = append(s1, 12)
fmt.Printf("修改后的 s1 值: %d\t长度: %d\t容量: %d\n", s1, len(s1), cap(s1))
s1 = append(s1, 14, 14)
fmt.Printf("修改后的 s1 值: %d\t长度: %d\t容量: %d\n", s1, len(s1), cap(s1))
}
输出结果如下:
初始 s1 值: [1 2 3 4 5 6] 长度: 6 容量: 9
修改后的 s1 值: [1 2 3 4 5 6 12] 长度: 7 容量: 9
修改后的 s1 值: [1 2 3 4 5 6 12 14 14] 长度: 9 容量: 9
在这个程序中,我们首先通过截取数组 a
创建了切片 s1
。s1
的范围是从索引 1 到索引 7。我们使用 :=
运算符将 append
的返回值赋值给 s1
。当我们使用 append
添加元素时,切片的容量会发生变化。如果元素数量超过容量,s1
的容量将翻倍。
切片的扩展规则如下:
注意: 切片的扩展并不总是翻倍,它取决于元素的大小、扩展的元素数量以及计算机的硬件等因素。更多信息请参考切片的高级部分。
Go 没有提供关键字或函数来删除切片中的元素,但我们可以使用数组截取来实现相同的功能,或者更强大的功能。
以下代码删除了切片 s
中索引 5 的元素,并将其赋值给新的切片 s1
:
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 删除前打印切片 `s`
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// 删除索引 5 的元素后打印切片 `s` 和 `s1`
fmt.Printf("%d\n%d\n", s, s1)
}
输出结果如下:
[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 6 7 8 9 9]
[0 1 2 3 4 6 7 8 9]
删除特定索引的关键在于 append
语句。它将索引 6 之后的元素追加到索引 5 之前的元素中。
这个操作相当于提前覆盖。例如,元素 5 被元素 6 覆盖,元素 6 被元素 7 覆盖,依此类推,直到元素 9 被添加到新切片中。因此,此时底层数组的值为 [0 1 2 3 4 6 7 8 9 9]
。
此时,如果我们检查 s1
的长度和容量,可以看到长度为 9,但容量为 10。这是因为 s1
是对 s
截取的引用。它们共享同一个底层数组。
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// 检查 s1 的长度和容量
fmt.Println(len(s1), cap(s1))
fmt.Printf("\n%d\n%d\n\n", s, s1)
// 修改切片 s
s[3] = 22
fmt.Printf("%d\n%d\n", s, s1)
}
输出结果如下:
[0 1 2 3 4 5 6 7 8 9]
9 10
[0 1 2 3 4 6 7 8 9 9]
[0 1 2 3 4 6 7 8 9]
[0 1 2 22 4 6 7 8 9 9]
[0 1 2 22 4 6 7 8 9]
除了删除特定位置的元素外,我们还可以使用截取从切片中删除特定范围的元素。操作与删除特定索引的元素相同。让我们做一个小练习。
创建一个文件 slice1.go
。创建一个切片 a
并按以下方式初始化。然后使用截取创建另一个切片 s
,其中不包括大于 3 或小于 7 的元素。最后,打印新的切片。
a := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
输出:
[9 8 7 3 2 1 0]
slice1.go
文件需要放在 ~/project
目录中。切片有两个属性:len
和 cap
。len
表示切片当前包含的元素数量,而 cap
表示切片可以容纳的最大元素数量。
当向切片添加的元素数量超过其容量时会发生什么?让我们一起来探索:
package main
import "fmt"
func main() {
s1 := make([]int, 3)
s2 := make([]int, 3, 5)
fmt.Println("s1 追加前:", s1, "长度:", len(s1), "容量:", cap(s1))
fmt.Println("s2 追加前:", s2, "长度:", len(s2), "容量:", cap(s2))
s1 = append(s1, 12)
s2 = append(s2, 22)
fmt.Println("s1 追加后:", s1, "长度:", len(s1), "容量:", cap(s1))
fmt.Println("s2 追加后:", s2, "长度:", len(s2), "容量:", cap(s2))
}
运行程序时,执行以下命令:
go run slice.go
输出结果如下:
s1 追加前: [0 0 0] 长度: 3 容量: 3
s2 追加前: [0 0 0] 长度: 3 容量: 5
s1 追加后: [0 0 0 12] 长度: 4 容量: 6
s2 追加后: [0 0 0 22] 长度: 4 容量: 5
正如我们在这个程序中看到的,当我们向切片添加元素并且元素数量超过其原始容量时,切片的容量会自动增加。
切片扩展的规则如下:
注意: 切片的容量并不总是翻倍,它取决于元素的大小、元素的数量以及计算机的硬件等因素。更多信息请参考切片的高级部分。
我们可以使用 copy
函数将一个切片复制到另一个切片中。语法如下:
func copy(dst, src []Type) int
dst
是目标切片,src
是源切片,最后的 int
表示复制的元素数量,即 len(dst)
和 len(src)
的最小值。
注意: copy
函数不会添加元素。
以下是一个示例:
package main
import "fmt"
func main() {
s1 := []int{0, 1, 2, 3}
s2 := []int{8, 9}
s3 := []int{0, 1, 2, 3}
s4 := []int{8, 9}
// 将 s1 复制到 s2
n1 := copy(s2, s1)
// 将 s4 复制到 s3
n2 := copy(s3, s4)
fmt.Println(n1, s1, s2)
fmt.Println(n2, s3, s4)
}
运行程序时,执行以下命令:
go run slice.go
输出结果如下:
2 [0 1 2 3] [0 1]
2 [8 9 2 3] [8 9]
在这个程序中,我们将切片 s1
的值复制到切片 s2
,并将切片 s4
的值复制到切片 s3
。copy
函数返回复制的元素数量。
我们注意到,s1
和 s2
的值相同,s3
和 s4
的值也相同。第一个 copy
函数将 s1[0, 1, 2, 3]
复制到 s2[8, 9]
。由于 s1
和 s2
的最小长度为 2,因此复制了 2 个值。目标切片 s2
被修改为 [0, 1]
。
第二个 copy
函数将 s4[8, 9]
复制到 s3[0, 1, 2, 3]
。由于 s3
和 s4
的最小长度为 2,因此复制了 2 个值。结果,s3
被修改为 [8, 9, 2, 3]
。
遍历切片与遍历数组类似。所有数组的遍历方法都可以用于切片。
在这个练习中,我们将测试并巩固对切片和数组遍历的理解。
创建一个文件 slice2.go
。声明一个数组 a1
和一个切片 s1
,并按以下方式初始化它们。然后,遍历数组 a1
和切片 s1
中的元素,并打印它们的索引和值。
a1 := [5]int{1, 2, 3, 9, 7}
s1 := []int{1, 8, 12, 1, 3}
输出:
数组 a1 中索引 0 的元素是 1
数组 a1 中索引 1 的元素是 2
数组 a1 中索引 2 的元素是 3
数组 a1 中索引 3 的元素是 9
数组 a1 中索引 4 的元素是 7
切片 s1 中索引 0 的元素是 1
切片 s1 中索引 1 的元素是 8
切片 s1 中索引 2 的元素是 12
切片 s1 中索引 3 的元素是 1
切片 s1 中索引 4 的元素是 3
slice2.go
文件需要放在 ~/project
目录中。range
格式或索引格式来遍历元素。在本节中,我们学习了切片及其用法。相比之下,切片比数组更加灵活和多功能。在处理多个切片时,我们需要谨慎以避免意外的切片操作。