《Go语言实战》笔记之第四章 ----数组、切片、映射

原文地址:

http://www.niu12.com/article/11

####数组

数组是一个长度固定的数据类型,用于存储一段具有相同的类型的元素的连续块。
数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型,
其占用的内存是连续分配的.
由于内存连续,CPU能把正在使用的数据缓存更久的时间。
而且内存连续很容易计算索引, 可以快速迭代数组里的所有元素。

声明:
两个要素:长度和类型

声明数组时需要指定内部存储的数据的类型, 以及需要存储的元素的数量,
这个数量也称为数组的长度

// 声明一个包含 5 个元素的整型数组
var array [5]int

一旦声明,数组里存储的数据类型和数组长度就都不能改变,元素为零值
如果需要存储更多的元素,
就需要先创建一个更长的数组,再把原来数组里的值复制到新数组里

// 声明一个包含 5 个元素的整型数组并初始化
array := [5]int{10, 20, 30, 40, 50}


...可替代长度,Go 语言会根据初始化时数组元素的数量来确定该数组的长度
// 容量由初始化值的数量决定
array := [...]int{10, 20, 30, 40, 50}

// 声明一个有 5 个元素的数组
// 用具体值初始化索引为 1 和 2 的元素
// 其余元素保持零值
array := [5]int{1: 10, 2: 20}
array[3] = 30
array值为: [0, 10, 20, 30 0]


指针数组:所有元素都是指针的数组
(指向整型的指针叫整型指针)
(指向字符串的指针叫字符串指针)
(......)
声明包含 5 个元素的指向整数的数组
// 用整型指针初始化索引为 0 和 1 的数组元素
array := [5]*int{0: new(int), 1: new(int)}
// 为索引为 0 和 1 的元素赋值
*array[0] = 10
*array[1] = 20
array值为: [0xc0420080a8 0xc0420080c0 <nil> <nil> <nil>]
<code>
array := [5]*int{0: new(int), 1: new(int)}
// 为索引为 0 和 1 的元素赋值
*array[0] = 10
*array[1] = 20
for _, p := range array {
if p != nil {
fmt.Println(*p)
} else {
fmt.Println(p)
}
}
// 输出 10 20 nil nil nil
</code>

数组的比较:
数组变量的类型包括 数组长度 和每个元素的 类型 。
只有这两部分都相同的数组, 才是类型相同的数组,才能互相赋值

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

var array3 [5]*string
array3 = array2 // error

在函数间传递数组:

根据内存和性能来看,在函数间传递数组是一个开销很大的操作。
在函数之间传递变量时,总是以值的方式传递的。
如果这个变量是一个数组,意味整个数组,不管有多长,都会完整复制,并传递给函数

最佳实践:传递数组的指针,这个操作会更有效地利用内存,性能也更好。
要意识到,因为现在传递的是指针,所以如果改变指针指向的值,会改变共享的内存

####切片slice

切片是动态数组,可以按需自动增长和缩小。
切片的动态增长是通过内置函数 append 来实现的。
这个函数可以快速且高效地增长切片。
还可以通过对切片再次切片来缩小一个切片的大小。
因为切片的底层内存也是在连续块中分配的,
所以切片还能获得索引、迭代以及为垃圾回收优化的好处。

声明:
两个必选要素: 类型与长度
一个可选要素: 容量

// 创建一个字符串切片
// 其长度和容量都是 5 个元素
slice := make([]string, 5)

分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,
但是初始化后并不能访问所有的数组元素

// 创建一个整型切片
// 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
for k := range slice {
fmt.Println(k) // 0 1 2
}
可以访问 3 个元素,而底层数组拥有 5 个元素。
剩余的 2 个元素可以在后期操作中合并到切片,可以通过切片访问这些元素
如果基于这个切片创建新的切片,新切片会和原有切片共享底层数组

len(array) <= cap(array)

使用切片字面量创建切片,同数组,只是不需要规定长度:
初始的长度和容量会基于初始化时提供的元素的个数确定

// 创建字符串切片
// 其长度和容量都是 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 整型切片
var slice []int
// true
fmt.Println(slice == nil)

// 使用 make 创建空的整型切片
slice2 := make([]int, 0)
// false
fmt.Println(slice2 == nil)

// 使用切片字面量创建空的整型切片
slice3 := []int{}
// false
fmt.Println(slice3 == nil)

切片赋值:
// 创建一个整型切片
// 其容量和长度都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 改变索引为 1 的元素的值
slice[1] = 25

使用切片创建切片:

// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素 [i:j]包i不包j
newSlice := slice[1:3]
fmt.println(newSlice) // [20, 30]

第一个切片 slice 能够看到底层数组全部 5 个元素的容量,
不过之后的 newSlice 就看不到。
对于 newSlice ,底层数组的容量只有 4 个元素。
newSlice 无法访问到它所指向的底层数组的第一个元素之前的部分。
所以,对 newSlice 来说,之前的那些元素就是不存在的。

现在两个切片共享同一个底层数组。
如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到

newSlice[0] = 1
fmt.Println(slice, newSlice) // [10 1 30 40 50] [1 30]

对底层数组容量是 k 的切片 slice[i:j]来说
长度: j - i
容量: k - i
对于 slice[i:j:k] 或 [2:3:4]
长度: j – i 或 3 - 2 = 1
容量: k – i 或 4 - 2 = 2
如果k - i大于可用容量,error: slice bounds out of range

切片只能访问到其长度内的元素。
试图访问超出其长度的元素将会导致语言运行时异常。
与切片的容量相关联的元素只能用于增长切片

切片增长:
用 append,需要一个被操作的切片和一个要追加的值

append 调用返回时,会返回一个包含修改结果的新切片。
函数 append 总是会增加新切片的长度,而容量有可能会改变,
也可能不会改变,这取决于被操作的切片的可用容量


// 创建一个整型切片
// 其长度和容量都是 5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为 2 个元素,容量为 4 个元素
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素
// 将新元素赋值为 60
newSlice = append(newSlice, 60)
// [10 20 30 60 50] [20 30 60]
fmt.Println(slice, newSlice)
因为 newSlice 在底层数组里还有额外的容量可用,
append 操作将可用的元素合并到切片的长度,
并对其进行赋值。由于和原始的 slice 共享同一个底层数组,
slice 中索引为 3 的元素的值也被改动了。

newSlice = append(newSlice, 60)
newSlice = append(newSlice, 60)
// 4
fmt.Println(cap(newSlice))
newSlice = append(newSlice, 60)
// 8
fmt.Println(cap(newSlice))

// [10 20 30 60 60] [20 30 60 60 60]
fmt.Println(slice, newSlice)
如果切片的底层数组没有足够的可用容量,
append 函数会创建一个新的底层数组,
将被引用的现有的值复制到新数组里,再追加新的值

函数 append 会智能地处理底层数组的容量增长。在切片的容量小于 1000 个元素时,总是
会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增
加 25%的容量。随着语言的演化,这种增长算法可能会有所改变。


内置函数 append 会首先使用可用容量。一旦没有可用容量,会分配一个
新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。
一旦发生这种情况,对切片进行修改,很可能会导致随机且奇怪的问题。
对切片内容的修改会影响多个切片,却很难找到问题的原因。
如果在创建切片时设置切片的容量和长度一样,
就可以强制让新切片的第一个 append 操作创建新的底层数组,
与原有的底层数组分离。
新切片与原有的底层数组分离后,可以安全地进行后续修改

内置函数 append 也是一个可变参数的函数。
这意味着可以在一次调用传递多个追加的值。
如果使用...运算符,可以将一个切片的所有元素追加到另一个切片里
// 创建两个切片,并分别用两个整数进行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果 [1 2 3 4]
fmt.Printf("%v\n", append(s1, s2...))

关键字 range配合关键字 for 来迭代切片里的元素
当迭代切片时,关键字 range 会返回两个值。
第一个值是当前迭代到的索引位置,
第二个值是该位置对应元素值的一份副本而不是直接返回对该元素的引用
可以使用空白标识符来忽略值

有两个特殊的内置函数 len 和 cap,可以用于处理数组、切片和通道

函数传递切片:
在函数间传递切片就是要在函数间以值的方式传递切片。
由于切片的尺寸很小,在函数间复制和传递切片成本也很低
在 64 位架构的机器上,一个切片需要 24 字节的内存:
指针字段需要 8 字节,长度和容量字段分别需要 8 字节
由于与切片关联的数据包含在底层数组里,不属于切片本身,
所以将切片复制到任意函数的时候,对底层数组大小都不会有影响。
复制时只会复制切片本身,不会涉及底层数组

####映射
映射是一种数据结构,用于存储一系列无序的键值对。
映射里基于键来存储值,映射功能强大的地方是,能够基于键快速检索数据。
映射的实现使用了散列表,所以映射是无序的集合

映射的散列表包含一组桶。在存储、删除或者查找键值对的时候,
所有操作都要先选择一个桶。把操作映射时指定的键传给映射的散列函数,
就能选中对应的桶。这个散列函数的目的是生成一个索引,
这个索引最终将键值对分布到所有可用的桶里。
随着映射存储的增加,索引分布越均匀,访问键值对的速度就越快
映射通过合理数量的桶来平衡键值对的分布。

/ 创建一个映射,键的类型是 string,值的类型是 int
dict := make(map[string]int)
// 创建一个映射,键和值的类型都是 string
// 使用两个键值对初始化映射
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,
只要这个值可以使用==运算符做比较

映射赋值:
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将 Red 的代码加入到映射
colors["Red"] = "#da1337"

从映射获取值并判断键是否存在
// 获取键 Blue 对应的值
value, exists := colors["Blue"]
通过键来索引映射时,即便这个键不存在也总会返回一个值。
在这种情况下,返回的是该值对应的类型的零值。

迭代映射里的所有值和迭代数组或切片一样,使用关键字 range

如果想把一个键值对从映射里删除,就使用内置的 delete 函也就是会每次增数

当传递映射给一个函数,并对这个映射做了修改时,
所有对这个映射的引用都会察觉到这个修改
这个特性和切片类似,保证可以用很小的成本来复制映射
posted @ 2019-01-11 13:27  周起  阅读(207)  评论(0编辑  收藏  举报