Skip to content

Go 语言实战 第 4 章 数组、切片和映射

🏷️ Go 《Go 语言实战》

数组

在 Go 语言里,数组是一个长度固定的数据类型,用户存储一段具有相同类型的元素的连续块
数组占用的内存是连续分配的。

声明数组时需要指定内部存储的数据的类型,以及需要存储的元素的数量。
一旦声明,数组里存储的数据类型和数组长度就都不能改变了。

go
var array [5]int

当数组初始化时,数组内每个元素都初始化为对应的零值。

go
var array [5]int
for _, val := range array {
    fmt.Println(val)
}

// print:
// 0
// 0
// 0
// 0
// 0

可以使用数组字面量快速创建数组并初始化。

go
array := [5]int{10, 20, 30, 40, 50}

使用 ... 代替数组长度,Go 语言会根据初始化时数组元素的数量来确定该数组的长度。

go
array := [...]int{10, 20, 30, 40, 50}

可以在数组字面量中指定元素值的下标:

go
array := [5]int{1: 10, 2: 20}

使用 [] 运算符访问数组指定下标的值:

go
array[3] = 30

在 Go 语言中,数组是一个值(这里的值是指值类型)。变量名代表整个数组,因此,同样类型的数组可以赋值给另一个数组。
数组的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值。

在下面的代码中,将 array2 复制给 array1

JiaJia:

这和 Java、C# 有很大的不同,在这些语言中,数组是引用类型,赋值时仅仅复制变量的引用到目标变量,赋值后两个变量指向内存中的同一个地址。而在 Go 语言中,由于数组是值类型,执行的是复制操作。

go
var array1 [5]string
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
array1 = array2

fmt.Printf("\narray1: %s\n", array1)
for i, val := range array1 {
    fmt.Printf("Value: %s Value-Addr: %X ElemAddress: %X\n", val, &val, &array1[i])
}

fmt.Printf("\narray2: %s\n", array2)
for i, val := range array2 {
    fmt.Printf("Value: %s Value-Addr: %X ElemAddress: %X\n", val, &val, &array2[i])
}

array1: [Red Blue Green Yellow Pink]
Value: Red Value-Addr: C00002E1F0 ElemAddress: C00003C050
Value: Blue Value-Addr: C00002E1F0 ElemAddress: C00003C060
Value: Green Value-Addr: C00002E1F0 ElemAddress: C00003C070
Value: Yellow Value-Addr: C00002E1F0 ElemAddress: C00003C080
Value: Pink Value-Addr: C00002E1F0 ElemAddress: C00003C090

array2: [Red Blue Green Yellow Pink]
Value: Red Value-Addr: C00002E250 ElemAddress: C00003C0A0
Value: Blue Value-Addr: C00002E250 ElemAddress: C00003C0B0
Value: Green Value-Addr: C00002E250 ElemAddress: C00003C0C0
Value: Yellow Value-Addr: C00002E250 ElemAddress: C00003C0D0
Value: Pink Value-Addr: C00002E250 ElemAddress: C00003C0E0

复制指针数组时,只会复制指针的值,而不会复制指针所指向的值。

JiaJia:

指针数组的赋值操作有些类似于 Java 中的数组赋值,但本质上完全不一样。一个是复制变量的地址,一个是复制每个数组项的地址。

多维数组

数组本身只有一个维度,不过可以组合多个数组创建多维数组。
多维数组很容易管理具有父子关系的数据或者坐标系相关联的数据。

只要类型一致,多维数组也可以互相赋值。
多维数组的类型包括每一维的长度以及最终存储在元素中的数据的类型

在函数间传递数组

根据内存和性能来看,在函数间传递数组是一个开销很大的操作。在函数之间传递变量时,总是以值的方式传递的。

切片

切片是一种数据结构,这种数据结构便于使用和管理数据集合。

切片有三个字段:

  1. 指向底层数组的指针
  2. 切片访问的元素的个数(即长度)
  3. 切片允许增长到的元素个数(即容量)

创建和初始化

  • slice := make([]string, 5):使用长度声明切片,切片的容量和长度相等;
  • slice := make([]int, 3 ,5):长度为 3,容量为 5 的整型切片;
  • slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}:切片字面量
  • slice := []string{99: ""}:使用索引声明切片

nil 切片和空切片

在声明时不做任何初始化,就会创建一个 nil 切片。

go
var slice []int

空切片在底层数组包含 0 个元素,也没有分配任何存储空间。

go
// 使用 make 创建空的整型切片
slice := make([]int, 0)

// 使用切片字面量创建空的整型切片
slice := []int{}

使用切片

使用切片字面量声明切片:

go
slice := []int{10, 20, 30, 40, 50}
slice[1] = 25

使用切片创建切片:

go
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]

切片只能访问其长度内的元素。

切片增长

相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量。

go
slice := []int{10, 20, 30, 40, 50}
// 长度为 2,容量为 4 的切片
newSlice := slice[1:3]
// 长度为 3,容量为 4 的切片
// 新元素值为 60
// slice[3] 的值也变为 60
newSlice = append(newSlice, 60)

如果切片的底层数组没有足够的可用容量, append 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值。

go
slice := []int{10, 20, 30, 40}
newSlice := append(slice, 50)

在切片的容量小于 1000 个元素时,总是会成倍的增加容量。一旦元素个数超过 1000,容器的增长因子会设为 1.25 ,也就是每次增加 25% 的容量。

创建切片时的第三个索引

第三个索引可以用来控制新切片的容量。

go
source := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 创建一个长度为 1,容量为 2 的切片
slice := source[2:3:4]

设置长度和容量一样的好处: 如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,就可以安全地进行后续修改。

使用 ... 运算符可以将一个切片的所有元素追加到另一个切片。

go
s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Printf("%v\n", append(s1, s2...))

迭代切片

go
slice := []int{10, 20, 30, 40}
for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}

需要强调的是:range 创建了每个元素的副本,而不是直接返回对元素的引用。

使用空白标识符(_)来忽略索引值

go
for _, value := range slice {
    fmt.Printf("Value: %d\n", value)
}

关键字 range 总是会从切片头部开始迭代。

函数 len 返回切片的长度,函数 cap 返回切片的容量。

多维切片

和数组一样,切片是一维的。不过,可以组合多个切片形成多维切片。

在函数间传递切片

在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片的成本也很低。

在 64 位架构的机器上,一个切片需要 24 字节的内容,指针字段需要 8 个字节,长度和容量字段分别需要 8 个字节。

映射

映射是一种数据结构,用于存储一系列无序的键值对。

映射的散列表包含一组桶。在存储、删除或者查找键值对时,所有操作都选择一个桶。
映射通过合理数量的桶来平衡键值对的分布。

对 Go 语言的映射来说,生成的散列键的一部分,具体来说是低位(LOB),被用来选择桶。

创建和初始化

go
// 创建一个映射,键的类型是 string,值的类型是 int
dict := make(map[string]int)
// 映射字面量
dict := map[string]int{"Red": "#da1337", "Orange": "#e95a22"}

映射的键可以是任何值,只要这个值可以使用 == 作比较。
切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误。

通过声明一个未初始化的映射来创建一个 nil 映射。nil映射不能用于存储键值对。

go
var colors map[string]string

从映射获取值并判断键是否存在:

go
value, exists := colors["Blue"]
if exists {
    fmt.Println(value)
}

从映射获取值,并通过该值判断键是否存在:

go
value := colors["Blue"]
if value != "" {
    fmt.Println(value)
}

在 Go 语言中,通过键来索引映射时,即使这个键不存在也总会返回一个值。
在这种情况下,返回的是该值对应的类型的零值。

使用内置的 delete 函数从映射中删除键值。

go
delete(colors, "Coral")

在函数间传递映射

在函数间传递映射并不会制造出该映射的一个副本。
当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改。

小结

  • 数组是构造切片和映射的基石。
  • Go 语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据。
  • 内置函数 make 可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片和映射字面量,或者使用字面量作为变量的初始值。
  • 切片有容量限制,不过可以使用内置的 append 函数扩展容量。映射的增长没有容量或者任何限制。
  • 内置函数 len 可以用来获取切片或者映射的长度。内置函数 cap 只能用于切片。
  • 通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值。但是切片不能用作映射的键。
  • 将切片或者映射传递给函数成本很小,并且不会复制底层的数据结构。