学习一下go语言基础所做笔记,主要参考《Go专家编程》和《Go程序设计语言》这两本书。
一 Slice
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。
数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。
指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。
1.1创建
(1)使用make
使用make来创建Slice时, 可以同时指定长度和容量, 创建时底层会分配一个数组, 数组的长度即容量。
例如, 语句 slice := make([]int, 5, 10) 所创建的Slice, 结构如下图所示:
(2)使用数组
使用数组来创建Slice时, Slice将与原数组共用一部分内存。
例如, 语句 slice := array[5:7] 所创建的Slice, 结构如下图所示:
切片从数组array[5]开始, 到数组array[7]结束( 不含array[7]) , 即切片长度为2, 数组后面的内容都作为切片的预留内存, 即capacity为5。数组和切片操作可能作用于同一块内存, 这也是使用过程中需要注意的地方。
1.2扩容
和java差不多,我画个流程图总结下:
二 Map
哈希表是一种巧妙并且实用的数据结构。它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。
map数据结构由 runtime/map.go/hmap 定义:
bucket数据结构由 runtime/map.go/bmap 定义(注意这是编译之后的):
下图展示bucket存放8个key-value对:
1.1插入过程
新员素插入过程如下:
- 跟据key值算出哈希值
他的计算结果是64位的,注意中间省略了一部分:
- 取哈希值低位与hmap.B取模确定bucket位置
假如现在hmap.B=2,就是我们之前的那张图2^2=4,我们取出后两位来做bucket的位置
- 查找该key是否已经存在, 如果存在则直接更新值
- 如果没找到将key, 将key插入到bucket的第一个空位,把tophash值设为hash值前八位。
1.2扩容
为了保证访问效率, 当新元素将要添加进map时, 都会检查是否需要扩容, 扩容实际上是以空间换时间的手段。 需要一个指标衡量,就是负载因子,也就是说什么情况下扩容?
(1)负载因子
负载因子用于衡量一个哈希表冲突情况, 公式为:
负载因子 = 元素数量/桶数量
例如, 对于一个bucket数量为4, 包含4个键值对的哈希表来说, 这个哈希表的负载因子为1。
哈希表需要将负载因子控制在合适的大小, 超过其阀值需要进行rehash, 也即键值对重新组织:
- 哈希因子过小, 说明空间利用率低
- 哈希因子过大, 说明冲突严重, 存取效率低
好的,我来帮你改写一下这段话:
Go 的默认负载因子是 6.5,而 Java 是 0.75。我觉得这主要是因为 Go 的空间利用率更高。Java 的哈希表就像是一个桶,每个桶里存放的是键值对链表(或者在某些情况下是红黑树)。相比之下,Go 的哈希表更像是一个桶的链表,每个桶可以容纳 8 个元素。这样算下来,0.75 乘以 8 等于 6,这和 Go 的 6.5 很接近。当然这只是我瞎想的。
(2)扩容的前提条件
触发扩容的条件有二个:
- 负载因子 > 6.5时, 也即平均每个bucket存储的键值对达到6.5个。
-
- overflow数量 > 2^15时, 也即overflow数量超过32768时。
(3)增量扩容
当负载因子过大时, 就新建一个bucket, 新的bucket长度是原来的2倍, 然后旧bucket数据搬迁到新的bucket。考虑到如果map存储了数以亿计的key-value, 一次性搬迁将会造成比较大的延时, Go采用逐步搬迁策略, 即每次访问map时都会触发一次搬迁, 每次搬迁2个键值对。
下图展示了包含一个bucket满载的map(为了描述方便, 图中bucket省略了value区域) :
当前map存储了7个键值对, 只有1个bucket。 此地负载因子为7。 再次插入数据时将会触发扩容操作, 扩容之后再将新插入键写入新的bucket。
当第8个键值对插入时, 将会触发扩容, 扩容后示意图如下:
hmap数据结构中oldbuckets成员指身原bucket, 而buckets指向了新申请的bucket。 新的键值对被插入新的bucket中。 后续对map的访问操作会触发迁移, 将oldbuckets中的键值对逐步的搬迁过来。 当oldbuckets中的键值对全部搬迁完毕后, 删除oldbuckets。
(4)等量扩容
所谓等量扩容, 实际上并不是扩大容量, buckets数量不变, 重新做一遍类似增量扩容的搬迁动作, 把松散的键值对重新排列一次, 以使bucket的使用率更高, 进而保证更快的存取。 在极端场景下, 比如不断的增删, 而键值对正好集中在一小部分的bucket, 这样会造成overflow的bucket数量增多, 但负载因子又不高, 从而无法执行增量搬迁的情况,如下图所示:
上图可见, overflow的buckt中大部分是空的, 访问效率会很差。 此时进行一次等量扩容, 即buckets数量不变,经过重新组织后overflow的bucket数量会减少, 即节省了空间又会提高访问效率。
1.3查找过程
- 跟据key值算出哈希值
- 取哈希值低位与hmpa.B取模确定bucket位置
- 取哈希值高位在tophash数组中查询
- 如果tophash[i]中存储值也哈希值相等, 则去找到该bucket中的key值进行比较
- 当前bucket没有找到, 则继续从下个overflow的bucket中查找。
- 如果当前处于搬迁过程, 则优先从oldbuckets查找注: 如果查找不到, 也不会返回空值, 而是返回相应类型的0值
画个流程图总结下: