Go 的数据类型 (数组,切片,和 集合映射)
如果使用数据库或者文件,或者访问网络,总需要一种方法来处理接收和发送的数据
Go 语言有 3 种数据结构可以让用户 管理集合数据
:数组
、切片
和 映射(集合)
1 数组 的内部实现和基础功能
内部实现
在Go 语言里,数组
是一个长度固定的数据类型,用于 存储一段 具有相同的类型的元素的连
续块。
数组存储的 类型可以是 内置类型,如整型或者字符串,也可以是某种结构类型。
数组是一种非常有用的数据结构,因为其占用的内存是连续分配的
。
由于 内存连续,CPU能把正在使用的数据缓存更久的时间。
而且 内存连续很容易计算索引, 可以快速迭代数组里的所有元素。数组的类型信息可以提供每次访问一个元素时需要在内存中移动的距离。
既然 数组的每个元素类型相同,又是连续分配,就可以 以固定速度索引数组中的任意数据,速度非常快。
声明和初始化
声明数组 时需要指定 内部存储的数据的类型
,以及需要 存储的元素的数量
一旦声明,数组里存储的数据类型和数组长度就都不能改变了。
var array [5] int
如果需要存储更多的元素,就需要先创建一个更长的数组,再把原来数组里的值复制到新数组里。
在 Go 语言中声明变量时,总会使用 对应类型的零值
来对变量 进行初始化
使用 数组字面量
声明和初始化 数组
array := [5] int {10, 20, 30, 40, 50}
array2 := [3] *string {new(string), new(string), new(string)}
// 容量由初始化值的数量决定
array := [...] int {10, 20, 30, 40, 50}
使用数组
因为 内存布局是连续的,所以 数组是效率很高的数据结构
. 要访问数组里某个单独元素,使用[ ]运算符
访问数组元素
// 用具体值初始为每个元素
array := [5]int{10, 20, 30, 40, 50}
// 修改索引为2的元素的值
array[2] = 35
访问指针数组的元素
// 用整型指针初始化索引为0 和1 的数组元素
array := [5]*int{0: new(int), 1: new(int)}
// 为索引为0 和1 的元素赋值
*array[0] = 10
*array[1] = 20
同样类型的数组可以赋值给另一个数组
数组变量的类型 包括 数组长度
和 每个元素的类型
。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值
把一个指针数组赋值给另一个
// 声明第一个包含3 个元素的指向字符串的指针数组
var array1 [3]*string
// 声明第二个包含3 个元素的指向字符串的指针数组
// 使用字符串指针初始化这个数组
array2 := [3]*string{new(string), new(string), new(string)}
// 使用颜色为每个元素赋值
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
// 将array2 复制给array1
array1 = array2
两组指向同样字符串的数组
函数间传递数组
根据 内存和性能来看,在函数间 传递数组 是一个开销很大的操作
。
在函数之间传递变量时,总是以值的方式传递
的。如果这个变量是一个数组,意味着整个数组,不管有多长,都会完整复
制,并传递给函数。
使用指针在函数间传递大数组
foo(&array)
// 函数foo 接受一个指向100 万个整型值的数组的指针
func foo(array *[1e6]int) {
...
一个8MB的内存的数组作为函数的参数 传递, 每次函数被调用的时候, 必须在栈上分配8MB的内存 。
而只传入指向数组的指针,这样只需要复制8 字节的数据而不是8 MB 的内存数据到栈上
这个操作会 更有效地利用内存,性能也更好。
不过要意识到,因为现在传递的是指针,所以如果改变指针指向的值
,会改变共享的内存
2 切片 的内部实现和基础功能
切片
是围绕 动态数组
的概念构建的,可以按需自动增长和缩小
。
切片的动态增长
是通过 内置函数append 来实现
的。这个函数可以快速且高效地 增长切片。
还可以通过对切片再次切片
来缩小一个切片的大小。
因为切片的底层内存也是在连续块中分配的,所以 切片还能获得索引、迭代以及为垃圾回收优化的好处
内部实现
切片有3 个字段的数据结构
,这些数据结构包含Go 语言需要操作底层数组的元数据
- 1 指向底层数组的指针
- 2 切片访问的元素的个数(即长度)
- 3 切片允许增长到的元素个数(即容量 )
声明,创建 和 初始化
内置的 make() 函数
内置的 make 函数。当使用 make 时,需要传入一个参数,指定切片的长度
slice := make ( []string, 5) 只指定长度,那么切片的 容量和长度相等
slice := make( [ ]int, 3, 5) // 其长度为3 个元素,容量为5 个元素
注意: 切片可以访问3 个元素,而底层数组拥有5 个元素
切片字面量
// 创建字符串切片
// 其长度和容量都是5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 创建一个整型切片
// 其长度和容量都是3 个元素
slice := []int{10, 20, 30}
// 使用索引声明切片
// 使用空字符串初始化第100 个元素
slice := []string{99: ""}
声明数组和声明切片的不同
// 创建有3 个元素的整型数组
array := [3]int{10, 20, 30}
// 创建长度和容量都是3 的整型切片
slice := []int{10, 20, 30}
nil 和 空切片
想 表示空集合 时空切片很有用,例如,数据库查询返回0 个查询结果时
var slice []int
slice := make([]int, 0)
slice := []int{}
使用切片
共享同一底层数组的两个切片
// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4 个元素
newSlice := slice[1:3]
需要记住的是,现在 两个切片共享同一个底层数组。如果 一个切片修改了该底层数组的共享部分,另一个切片也能感知到
切片的增长( append 新建一个切片 )
相对于数组而言,使用切片的一个好处是,可以按需增加切片的容量
。
Go 语言内置的 append函数
会处理增加长度时的所有操作细节。
要使用 append,需要一个被操作的切片和一个要追加的值当append 调用返回时,会返回一个包含修改结果的新切片
。
函数append 总是会增加新切片的长度,而容量有可能会改变,也可能不会改变,这取决于被操作的切片的可用容量。
切片在底层数组 还有额外的容量可以使用
// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4 个元素 ( 低层的数组容量 - 切片的索引 )
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素
// 将新元素赋值为60
newSlice = append(newSlice, 60) // 还有容量可以使用
append 操作将可用的元素合并到切片的长度,并对其进行赋值。由于和原始的slice 共享同一个底层数 组,slice 中索引为3 的元素的值也被改动了。
切片在底层数组 没有额外的容量可以使用 ( 切片容量 翻倍--小于1000个的时候 )
// 创建一个整型切片
// 其长度和容量都是4 个元素
slice := []int{10, 20, 30, 40}
// 向切片追加一个新元素
// 将新元素赋值为50
newSlice := append(slice, 50)
函数append 会智能地处理底层数组的容量增长
- 在 切片的容量小于1000 个元素时,总是会成倍地增加容量。
- 一旦元素个数 超过1000,容量的增长因子会设为1.25,也就是会每次增加25%的容量。
设置长度和容量一样的好处
slice := source [2:3:3]
如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个append 操作
创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行
后续修改
切片的迭代
使用for range 迭代切片
slice := []int{10, 20, 30, }
// 迭代每一个元素,并显示其值
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
Output:
Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
关键字range 会返回两个值。第一个值是 当前迭代到的索引位置,第二个值是该位置 对应元素值的一份副本( 副本的地址相同,不是直接返回该元素的引用)
使用传统的for 循环对切片进行迭代
// 创建一个整型切片
// 其长度和容量都是4 个元素
slice := []int{10, 20, 30, 40}
// 从第三个元素开始迭代每个元素
for index := 2; index < len(slice); index++ {
fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}
Output:
Index: 2 Value: 30
Index: 3 Value: 40
函数的传递切片
在函数间传递切片就是要在函数间以值的方式传递切片。由于 切片的尺寸很小,在函数间复制和传递切片成本也很低
。
一个切片需要 24 字节的内存:指针字段需要8 字节,长度 和 容量字段分别需要8 字节。
由于 与切片关联的数据包含在 底层数组里,不属于切片本身,所以将切片复制到任意函数的时候, 对底层数组大小都不会有影响。复制时只会复制切片本身,不会涉及底层数组
在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方
。不需要传递指
针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,然后传递回一份新的切片副本
。
3 映射 的内部实现和基础功能
映射
是一种数据结构,用于 存储一系列无序的键值对
。
映射里基于键来存储值
。
映射功能强大的地方是,能够 基于键 快速检索数据 。
键就像索引一样,指向与该键关联值。
内部实现
映射是一个集合,可以使用 类似处理数组和切片的方式 迭代映射中的元素。但 映射是 无序的集合,意味着 没有办法预测键值对被返回的顺序。
即便使用同样的顺序保存键值对,每次迭代映射的时候顺序也可能不一样。
无序的原因
是 映射的实现 使用了散列表
对Go 语言的映射来说,生成的散列键的一部分,具体来说是`低位(LOB),被用来选择桶`。
第一个数据结构是一个数组,内部存储的是用于选择桶的 散列键的高八位值。这个数组用于区分每个
键值对要存在哪个桶里。
第二个数据结构是一个字节数组,用于存储键值对。该字节数组先依次存储了这个桶里所有的键,之后依次存储了这个桶里所有的值。
实现这种键值对的存储方式目的在于减少每个桶所需的内存。
创建 和 初始化
映射的键 可以是任何值
这个值的类型可以是 内置的类型,也可以是 结构类型,
只要这个值可以使用==运算符做比较
注意:
切片、函数 以及 包含切片的结构类型 这些类型 由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误
make( ) 函数
使用make 声明映射
// 创建一个映射,键的类型是string,值的类型是int
dict := make(map[string]int)
// 创建一个映射,键和值的类型都是string
// 使用两个键值对初始化映射
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
映射字面量
// 创建一个映射,使用字符串切片作为值
dict := map[int][]string{}
使用映射
可以通过声明一个未初始化的映射来创建一个值为 nil 的映射(称为nil 映射)。 nil 映射不能用于存储键值对,否则,会产生一个语言运行时错误。
对nil 映射赋值错误
// 通过声明映射创建一个nil 映射
var colors map[string]string
// 将Red 的代码加入到映射
colors["Red"] = "#da1337"
Runtime Error:
panic: runtime error: assignment to entry in nil
正确的赋值操作
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将Red 的代码加入到映射
colors["Red"] = "#da1337"
判断映射的键是否存在
1 // 获取键 Blue 对应的值
value, exists := colors["Blue"]
// 这个键存在吗?
if exists {
fmt.Println(value)
}
2 // 获取键Blue 对应的值
value := colors["Blue"]
// 这个键存在吗?
if value != "" {
fmt.Println(value)
}
通过键来索引映射时,即便 这个键不存在也总会返回一个值。 在这种情况下,返回的是该值对应的类型的零值 ( 对应类型的零值 “” 或者 0 , nil )
迭代映射
使用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)
}
删除映射中的元素 (这种方法只能用在映射存储的值都是非零值的情况。)
从映射中删除一项
// 删除键为Coral 的键值对
delete(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
映射函数传递
在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,并对 这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改
package main
import "fmt"
func main() {
// 创建一个映射,存储颜色以及颜色对应的十六进制代码
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)
}
// 调用函数来移除指定的键
removeColor(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
}
// removeColor 将指定映射里的键删除
func removeColor(colors map[string]string, key string) {
delete(colors, key)
}
小结
数组 是 构造切片 和 映射的基石。
Go 语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据。
内置函数make 可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片 和映射字面量,或者使用字面量作为变量的初始值。
切片有容量限制,不过可以使用 内置的 append 函数扩展容量。
映射的增长没有容量或者任何限制。
内置函数len 可以用来获取切片或者映射的长度。
内置函数cap 只能用于切片。
通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值。但是切片不能用作映射的键。
将切片或者映射传递给函数成本很小,并且不会复制底层的数据结构。