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 语言很重要的一步。