3. 数组、切片和字典
上一章中介绍了 Go 语言中的基本数据类型,本章将会介绍 Go 语言中内置的三种常用复合数据类型:数组(array)、切片(slice)和字典(map),这三种数据结构都可以用于保存多项数据,其中数组和切片较为相似,都可以按顺序保存元素,并可以通过索引访问元素,不同的是数组的长度是不可修改的,而切片可通过增加、删除等修改改变切片的长度。字典中存储的则是 key-value 类型的数据。
3.1. 数组
Go 语言中,数组是一系列同一类型数据的集合,数组中每个数据被称为元素,一个数组中包含的元素的个数成为数组的长度。
3.1.1. 声明数组
数组的常用声明方式如下:
var arr [5]int // 一维
var arr2 [5][5]int // 二维
数组一旦声明完成,数组的长度也就固定了,后面不能再修改。如上,声明的数组 arr 的长度为 5,声明的二维数组 arr2 的第一维和第二维的长度都是 5。
3.1.2. 数组初始化
声明时并同步初始化
var arr = [5]int{1, 2, 3, 4, 5}
// 或 arr := [5]int{1, 2, 3, 4, 5}
或者可以使用[]运算符,索引或者修改数组
arr := [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(arr); i++ {
arr[i] += 100
}
fmt.Println(arr) // [101 102 103 104 105]
这里使用到了 for 循环,这个在后续的章节中会详细介绍到。
3.1.3. 数组的遍历
数组的遍历通常可采用 for 循环的方式,上面已经介绍到,我们再看一下如下的代码:
func main() {
a := [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
}
在代码中,使用到了 len() 函数,用于获取数组的长度。[]运算符用于取到对应索引的内容。除了上述的 for 循环的方式,在 Go 语言中,还支持 for...range 的语法,具体例子如下:
func main() {
a := [5]int{1, 2, 3, 4, 5}
for index, value := range a {
fmt.Printf("index: %d, value: %d\n", index, value)
}
}
如果只想获取 value 的值,可以使用 _ 占位符,具体代码如下:
func main() {
a := [5]int{1, 2, 3, 4, 5}
for _, value := range a {
fmt.Println(value)
}
}
上述都是一维数组的遍历,对于二维或者二维以上数组的遍历,需要双层或者多层的 for 循环,具体代码如下:
func main() {
a := [2][5]int{{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}}
for i := 0; i < len(a); i++ {
for j := 0; j < len(a[i]); j++ {
fmt.Printf("%d ", a[i][j])
}
fmt.Println()
}
}
或者使用 for range 的方式:
func main() {
a := [2][5]int{{1, 2, 3, 4, 5}, {6, 7, 8, 9, 10}}
for _, value1 := range a {
for _, value2 := range value1 {
fmt.Printf("%d ", value2)
}
fmt.Println()
}
}
在使用 for range 时,range 的值是原数组的拷贝,简单来说就是在 for range 中对 value 的修改不会影响到原数组,具体如下:
func main() {
a := [5]int{1, 2, 3, 4, 5}
for _, value := range a {
value = value * 2
}
fmt.Println(a) // [1 2 3 4 5]
}
上述代码中对 value 修改,并没有改变数组 a 的值。
3.2. 切片
数组存在的问题是当数组声明后,其大小不能再变化。要想使用大小可变的数组,这个时候就需要用到切片。切片是数组的抽象,底层结构依旧是数组,与数组不同的是切片可以随时根据需求进行扩展。
3.2.1. 创建切片
创建数组切片的方法主要有两种,分别是基于数组和直接创建。首先,我们看下如何基于数组创建切片,代码如下:
func main() {
a := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sliceA := a[2:5]
fmt.Println("Slice A:", sliceA) // [3 4 5]
}
这里,我们通过数组 a 创建了切片 sliceA,同时,也可以基于切片创建切片,代码如下:
func main() {
a := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sliceA := a[2:5]
fmt.Println("Slice A:", sliceA) // [3 4 5]
sliceB := sliceA[1:3]
fmt.Println("Slice B:", sliceB) // [4 5]
}
我们基于切片 sliceA 又创建了切片 sliceB。除了上述基于数组,基于切片创建切片之外,还可以直接创建切片,代码如下:
func main() {
a := []int{} // 空的切片
fmt.Println(a) // []
b := []int{1, 2, 3} // 包含元素的切片
fmt.Println(b) // [1 2 3]
}
另一种切片的创建方法是使用 make() 函数,代码如下:
func main() {
a := make([]int, 3, 5)
fmt.Println(a) // [0 0 0]
}
在 make() 函数的参数中,分了三个部分:
- 第一个部分:
[]int,表示的是切片类型,必填参数 - 第二个部分:3,当前长度 len,必填参数
- 第三个部分:5,容量 cap,选填参数
这里要区分清楚当前长度和容量的概念。容量是指一共申请了的空间大小,而当前长度是当前已经使用的大小,这就不难发现:0 <= len <= cap。
3.2.2. 切片的遍历
切片的遍历方法与数组的遍历方法一致,可以通过 for 循环或者 for...range 的方法遍历,具体如代码所示:
func main() {
a := []int{1, 2, 3, 4, 5}
for i:=0; i < len(a); i++ {
fmt.Println(a[i])
}
fmt.Println()
for _, value := range a {
fmt.Println(value)
}
}
3.2.3. 向切片中添加元素
向已有的切片中添加元素可以通过 append() 方法,具体代码如下:
func main() {
a := make([]int, 3, 5)
for i := 0; i < len(a); i++ {
a[i] = i
}
fmt.Println(a) // [0, 1, 2]
a = append(a, 3)
fmt.Println(a) // [0, 1, 2, 3]
}
我们知道,切片 a 在创建时,容量 cap 的大小为 5,此时当前长度=容量的情况下,再添加元素会发生什么情况,如下代码:
func main() {
// 初始化一个长度为5,容量为5的切片
a := make([]int, 5, 5)
for i := 0; i < len(a); i++ {
a[i] = i
}
fmt.Printf("切片a: %v, 长度: %d, 容量: %d\n", a, len(a), cap(a))
a = append(a, 6)
fmt.Printf("切片a: %v, 长度: %d, 容量: %d\n", a, len(a), cap(a))
}
执行后,发现:
切片a: [0 1 2 3 4], 长度: 5, 容量: 5
切片a: [0 1 2 3 4 6], 长度: 6, 容量: 10
当长度 > 容量时,会重新分配一个新的空间。那如果切片是基于数组创建的,先看最基本的情况,如下代码:
func main() {
// 数组 a
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 切片 sliceA
sliceA := a[2:5]
fmt.Printf("切片 sliceA: %v, 长度: %d, 容量: %d\n", sliceA, len(sliceA), cap(sliceA))
}
切片 sliceA 的长度很好理解,测试一下,容量为 8,这是因为切片是基于数组创建的,切片的容量不是切片中元素的个数,而是从切片起始位置到底层数组末尾的元素个数。此时,在切片中增加元素,会发生什么情况,如下代码:
func main() {
// 数组 a
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 切片 sliceA
sliceA := a[2:5]
fmt.Printf("切片 sliceA: %v, 长度: %d, 容量: %d\n", sliceA, len(sliceA), cap(sliceA))
sliceA = append(sliceA, 10)
fmt.Printf("切片 sliceA: %v, 长度: %d, 容量: %d\n", sliceA, len(sliceA), cap(sliceA))
fmt.Printf("数组 a: %v\n", a)
}
执行代码,结果如下:
切片 sliceA: [2 3 4], 长度: 3, 容量: 8
切片 sliceA: [2 3 4 10], 长度: 4, 容量: 8
数组 a: [0 1 2 3 4 10 6 7 8 9]
我们发现原数组也被修改了,如果切片长度=容量的情况下,再增加元素会发生什么情况,如下代码:
func main() {
// 数组 a
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 切片 sliceA
sliceA := a[2:10]
fmt.Printf("切片 sliceA: %v, 长度: %d, 容量: %d\n", sliceA, len(sliceA), cap(sliceA))
sliceA = append(sliceA, 10)
fmt.Printf("切片 sliceA: %v, 长度: %d, 容量: %d\n", sliceA, len(sliceA), cap(sliceA))
fmt.Printf("数组 a: %v\n", a)
}
执行代码后,结果如下:
切片 sliceA: [2 3 4 5 6 7 8 9], 长度: 8, 容量: 8
切片 sliceA: [2 3 4 5 6 7 8 9 10], 长度: 9, 容量: 16
数组 a: [0 1 2 3 4 5 6 7 8 9]
我们发现,当未超出容量的情况下,会对原先的数组修改,具体的过程如下图所示:

当切片在超出容量后,会重新申请新的空间,具体过程如下图所示:

有了上述的直观的理解,再看一下切片的定义,包含了三个部分,指向特定空间的指针(指针的概念在后面章节会介绍),长度以及容量,具体的定义如下:
type slice struct {
ptr *T
len int
cap int
}
3.2.4. 合并多个切片
除了向切片中增加元素之外,还可以使用 append() 函数合并多个切片,具体代码如下所示:
func main() {
// 切片 a
a := []int{0, 1, 2, 3, 4}
// 切片 b
b := []int{5, 6, 7, 8, 9}
// 合并切片 a 和 b
c := append(a, b...)
fmt.Println("切片 a:", a)
fmt.Println("切片 b:", b)
fmt.Println("合并后的切片 c:", c) // [0 1 2 3 4 5 6 7 8 9]
}
注意:在代码 c := append(a, b...) 的 b 的后面有 ...,如果不加这个会报错:
cannot use b (variable of type []int) as int value in argument to append
实际上,... 叫做可变参数展开(variadic expansion),意思是:把切片 b 中的所有元素逐个取出来,作为多个参数传给 append。因此上述的代码等价于:
c := append(a, 5, 6, 7, 8, 9)
3.2.5. 删除切片中的元素
在切片中没有专门的删除元素的函数,但是可以使用重新拼接切片的方式实现删除切片中元素的效果,具体代码如下:
func main() {
// 切片 a
a := []int{0, 1, 2, 3, 4}
// 删除元素 3
b := append(a[:3], a[4:]...)
fmt.Println(b) // [0 1 2 4]
}
3.3. 字典
字典 map 是一种用于存储键值对(Key-Value)的数据解构,在其他语言中也有对应的结构,如在 Java 语言中有 HashMap,在 Python 语言中有 dict。
3.3.1. 声明和初始化
与切片一样,Go 语言中的字典可以通过 make 函数声明,具体如下代码:
func main() {
score := make(map[string]int)
score["语文"] = 90
score["数学"] = 95
score["英语"] = 85
fmt.Println(score) // map[数学:95 英语:85 语文:90]
}
在 make 函数中,创建了一个 map。其中 string 称之为 key,int 称之为 value。除了使用 make 函数声明外,也可以简化,直接使用字面量初始化,具体代码如下:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
fmt.Println(score) // map[数学:95 英语:85 语文:90]
}
3.3.2. 获取元素
首先是获取元素,对于字典,可以直接根据 key 获取对应的 value,具体代码如下:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
score_english := score["英语"]
fmt.Printf("英语成绩: %d\n", score_english) // 英语成绩: 85
}
3.3.3. 判断 key 是否存在
上面获取元素默认是 key 存在的情况下,如果 key 不存在,会发生什么情况,如下代码:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
scorePhysics := score["物理"]
fmt.Println(scorePhysics) // 0
}
我们发现,在 key 不存在的情况下,Go 语言并不会报错,而是返回对应数据类型的 0 值,如上 int 型返回 0。那么,需要判断指定 key 是否存在,有如下代码:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
scorePhysics, isExist := score["物理"]
fmt.Println("物理成绩存在:", isExist, ", 成绩:", scorePhysics) // false, 0
}
3.3.4. 删除元素
与切片不同的是,在字典中提供了 delete 函数,用于删除指定的 key-value 对,具体的代码如下:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
// 删除存在的 key-value 对
delete(score, "英语")
fmt.Println(score)
// 删除不存在的 key-value 对,不会报错
delete(score, "体育")
fmt.Println(score)
}
注意:在字典中,删除不存在的 key-value 对,并不会报错。
3.3.5. 修改元素
修改元素,可以通过对应的 key 直接修改 value 的值,具体的代码如下:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
score["英语"] = 88
fmt.Println(score)
}
3.3.6. 增加 key-value 数据对
直接对指定的 key 赋值可以实现新增 key-value 对,具体代码如下:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
// 添加新的键值对
score["体育"] = 88
fmt.Println(score)
}
3.3.7. 获取长度
可以使用 len() 函数获取字典的长度,具体代码如下:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
fmt.Println(len(score))
}
3.3.8. 遍历 map
如数组或者切片一样,使用 for...range 可以遍历字典,具体代码如下:
func main() {
score := map[string]int{
"语文": 90,
"数学": 95,
"英语": 85,
}
for key, value := range score {
fmt.Printf("%s: %d\n", key, value)
}
}
至此,对于字典中的常用操作就介绍完了,字典中仍有一些其他的概念,我们会在后面涉及到的时候会介绍。
3.4. 本章小结
数组,切片和字典是 Go 语言中重要的数据结构,这三种数据结构都可以用于保存多项数据,其中数组和切片较为相似,都可以按顺序保存元素,并可以通过索引访问元素,不同的是数组的长度是不可修改的,而切片可通过增加、删除等修改改变切片的长度。字典中存储的则是 key-value 类型的数据。掌握这三种数据结构是掌握 Go 语言很重要的一步。