3. 数组、切片和映射
主要内容:
- 数组的内部实现和基础功能
- 使用切片管理数据集合
- 使用映射管理键值对
- Go语言有三种数据结构可以让用户管理集合数据:数组、切片和映射。
- 补充:指针和struct
1. 数组
1.1 数组的内部实现和基础功能
数组是切片和映射的基础数据结构
- 在Go语言里,数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块。数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。
- 声明数组时需要指定内部存储的数据的类型,以及需要存储的元素的数量,这个数量也称为数组的长度。
var arr [5]int
- 一旦声明,数组里存储的数据类型和数组长度就都不能改变了。如果需要存储更多的元素,就需要先创建一个更长的数组,再把原来数组里的值复制到新数组里。
- 一种快速创建数组并初始化的方式是使用数组字面量。
arr2 := [5]int{1, 2, 3, 4, 5}
- 如果使用
...
替代数组的长度,Go 语言会根据初始化时数组元素的数量来确定该数组的长度。arr3 := [...]int{6, 7, 8}
- 声明数组并指定特定元素的值:
arr4 := [5]int{1:10, 2:20}
- 声明一个所有元素都是指针的数组。使用
*
运算符就可以访问元素指针所指向的值。 - 数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值。
- 声明二维数组
- 声明一个二维整型数组,两个维度分别存储4个元素和2个元素:
var arr5 [4][2]int
- 使用数组字面量来声明并初始化一个二维整型数组:
arr6 := [4][2]int{{1, 2}, {3, 4}, {5, 6}, {7, 8}}
- 声明并初始化外层数组中索引为 1 个和 3 的元素:
arr7 := [4][2]int{1: {11, 12}, 3: {44, 45}}
- 声明并初始化外层数组和内层数组的单个元素:
arr8 := [4][2]int{1: {1: 12}, 3: {1: 13}}
- 声明一个二维整型数组,两个维度分别存储4个元素和2个元素:
- ==在函数之间传递变量时,总是以值的方式传递的。==如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。
- 有一种更好且更有效的方法来处理这个操作。可以只传入指向数组的指针,这样只需要复制 8 字节的数据。
1.2数组的相关操作
(1)数组元素的访问:
a := [...]int{1, 2, 3, 4, 5}
b := a[0]
fmt.Println(b)
for i, v := range a {
fmt.Println(i, v)
}
(2)数组的长度
alength := len(a)
for i := 0; i < alength; i++ {
fmt.Println(a[i])
}
2. 切片的内部实现和基础功能
切片是一种数据结构,便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数append来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。
2.1 内部实现
切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有3个字段的数据结构,这些数据结构包含Go语言需要操作底层数组的元数据。
这3个字段分别是指向底层数组的指针
、切片访问的元素的个数
(即长度)和切片允许增长到的元素个数
(即容量)。
2.2 创建和初始化
是否能提前知道切片需要的容量通常会决定要如何创建切片。
- make和切片字面量
一种创建切片的方法是使用内置的make函数,当使用make时,需要传入一个参数,指定切片的长度。
// 创建一个字符串切片,其长度和容量都是5个元素
slice := make([]string, 5)
如果只指定长度,那么切片的容量和长度相等。同样,也可以分别指定长度和容量。
slice := make([]int, 3,5)
分别指定长度和容量时,创建的切片,底层数据的长度是指定的容量,但是初始化后不能访问所有的数组元素。
注意:不允许创建容量小于长度的切片。
-
另一种常用的创建切片的方法是使用切片字面量。初始的长度和容量会基于初始化时提供的元素的个数确定。
slice := []string{"R", "B", "D", "P", "Y", "L"}
当使用切片字面量时,可以设置初始长度和容量。要做的就是在初始化时给出所需的长度和容量作为索引。
sllice := []string{99:""}
注意:如果在[]运算符里指定了一个值,那么创建的就是数组而不是切片。 -
创建nil切片
var slice []int
-
利用初始化,通过声明一个切片可以创建一个空切片。
slice := make([]int, 0)
或者slice := []int{}
-
对切片里某个索引指向的元素赋值和对数组里某个索引指向的元素赋值的方法完全一样。使用[]操作符就可以改变某个元素的值。
-
切片之所以被称为切片,是因为创建一个新的切片就是把底层数组切出一部分。
// 创建一个整型切片, 其长度和容量都是5个元素
slice3 := []int{1, 2, 3, 4, 5}
// 创建一个新切片, 其长度为2个元素,容量为4个元素
newslice := slice3[1:3]
fmt.Println(slice3)
fmt.Println(newslice)
注意:
(1)newSlice 无法访问到它所指向的底层数组的第一个元素之前的部分。
(2)对底层数组容量为k的切片slice[i:j]来说:长度为:j-i;容量为:k-i
(3)现在两个切片共享同一个底层数组。如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到。
(4)切片只能访问到其长度内的元素。试图访问超出其长度的元素将会导致语言运行时异常。
(5)相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量。
2.3 切片的相关操作
- 切片增长
- 函数
append
总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量。 - 如果切片的底层数组没有足够的可用容量,append函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值。
- 函数append会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25%的容量。随着语言的演化,这种增长算法可能会有所改变。
- 第三个索引可以用来控制新切片的容量。其目的并不是要增加容量,而是要限制容量。
// 创建字符串切片,其长度和容量都是6个元素
slice := []string{"R", "B", "D", "P", "Y", "L"}
//将第三个元素切片,并限制容量,其长度为1个元素,容量为2个元素
newslice := slice[2:3:4]
// 对于slice[i:j:k],例如slice[2:3:4]
// 长度:j-i 或 3-2 = 1
// 容量:k-i 或 4-2 = 2
- 如果试图设置的容量比可用的容量还大,就会得到一个语言运行时错误。
- 如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改。
// 创建字符串切片,其长度和容量都是 5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 对第三个元素做切片,并限制容量,其长度和容量都是 1 个元素
slice := source[2:3:3]
// 向 slice 追加新字符串
slice = append(slice, "Kiwi")
- 内置函数 append 也是一个可变参数的函数。这意味着可以在一次调用传递多个追加的值。如果使用…运算符,可以将一个切片的所有元素追加到另一个切片里。
// 创建两个切片,并分别用两个整数进行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果
fmt.Printf("%v\n", append(s1, s2...))
- 迭代切片
- 当迭代切片时,关键字
range
会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。range 创建了每个元素的副本,而不是直接返回对该元素的引用。
(1)for range迭代切片
// 创建一个整型切片 ,其长度和容量都是 4 个元素
slice4 := []int{10, 20, 30, 40}
// 迭代每一个元素,并显示其值
for index, value := range slice4 {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
(2)传统for循环迭代切片
// 创建一个整型切片,其长度和容量都是 4 个元素
slice5 := []int{1, 2, 3, 4}
// 从第三个元素开始迭代每个元素
for index := 0; index < len(slice5); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice5[index])
}
- 有两个特殊的内置函数
len
和cap
,可以用于处理数组、切片和通道。对于切片,函数len返回切片的长度
,函数cap 返回切片的容量
。 - 多维切片
// 创建一个整型切片的切片
slice6 := [][]int{{10}, {100, 200}}
- 在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低。
// 分配包含 100 万个整型值的切片
slice := make([]int, 1e6)
// 将 slice 传递到函数 foo
slice = foo(slice)
// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
}
- 在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量字段分别需要 8 字节。由于与切片关联的数据包含在底层数组里,不属于切片本身,所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组。
- 在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本。
3. 映射
Go语言内置的字典类型叫map
。map的类型格式是:map[K]T,其中K可以是任意可以进行比较的类型,T是值类型。map也是一种引用类型。
- 映射里基于键来存储值。映射功能强大的地方是,能够基于键快速检索数据。键就像索引一样,指向与该键关联的值。
- 映射是无序的集合。无序的原因是映射的实现使用了散列表。
3.1 map的创建
- 使用字面量创建
ma := map[string]int{"A": 1, "B": 2}
fmt.Println(ma)
fmt.Println(ma["A"])
- 使用内置的make函数创建
// 创建一个映射,键的类型是 string,值的类型是 int
dict := make(map[string]int)
注意:
(1)映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,只要这个值可以使用==运算符做比较。
(2)切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误
- 声明一个存储字符串切片的映射:
dict := map[int][]string{}
- 空映射和映射赋值
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将 Red 的代码加入到映射
colors["Red"] = "#da1337"
- nil映射
// 通过声明映射创建一个 nil 映射
var colors map[string]string
// 将 Red 的代码加入到映射
colors["Red"] = "#da1337"
Runtime Error:
panic: runtime error: assignment to entry in nil map
3.2 映射的相关操作
- 从映射获取值并判断键是否存在
value1, exists := color["Red"]
if exists {
fmt.Println(value1)
}
- 从映射获取值,并通过该值判断键是否存在
value2 := color["Red"]
if value2 != "" {
fmt.Println(value2)
}
- 使用range迭代映射
// 创建一个映射,存储颜色以及颜色对应的十六进制代码
colors := map[string]string{
"AliceBlue": "#f0f8ff",
"Coral": "#ff7F50",
"DarkGray": "#a9a9a9",
"ForestGreen": "#228b22",
}
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
- 如果想把一个键值对从映射里删除,就使用内置的 delete 函数。
delete(colors, "AliceBlue")
- 在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改。
4. 指针
Go语言支持指针,指针的声明类型为*T,Go同样支持多级指针**T。通过在变量名前加&
来获取变量的地址。
- 在赋值语句中,
*T
出现在"=“左边表示指针声明,*T
出现在”="右边表示获取指针指向的值。 - 结构体指针访问结构体字段仍然使用“.”点操作符。
- Go不支持指针的运算。
- 函数中允许返回局部变量的地址。
fmt.Println(*sum(1, 3))
func sum(a, b int) *int {
sum := a + b
return &sum
}
5. struct
Go中的struct与C类似,由多个不同类型元素组合而成,有两层含义:①struct结构中的类型可以是任意类型;②struct的存储空间是连续的,其字段按照声明时的顺序存放(注意字段之间有对齐要求)
struct有两种形式:一种是struct类型字面量,另一种是使用type声明的自定义struct类型。
(1)struct类型字面量
struct {
FieldName FieldType
FieldName FieldType
FieldName FieldType
}
(2)自定义struct类型
type TypeName struct {
FieldName FieldType
FieldName FieldType
FieldName FieldType
}
实际中,更多是使用type自定义一个新的类型来实现。type是自定义类型的关键字,不但支持struct类型的创建,还支持任意其他自定义类型的创建。
(3)struct类型的初始化
p := &Person{
Name: "Jason",
Age: 12,
}
s := Student{
Person: p,
Number: 11,
}
fmt.Println(s.Name, s.Age, s.Number)
type Person struct {
Name string
Age int
}
type Student struct {
*Person
Number int
}
至此,我们已经了解了Go语言的基本数据类型,下面,我们将学习Go语言中的函数类型。
6. 参考资料
- 《Go语言实战》
- 《Go语言核心编程》