Go 语言 Map(集合) | 菜鸟教程 (runoob.com)
简述
Map 是一种无序的键值对的集合。
map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的。
在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 ""。
Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量。
Map 类型可以写为map[K]V。其中K对应的key必须是支持 == 比较运算符的数据类型,所以 Map 可以通过测试key是否相等来判断是否已经存在。
map类型的零值是nil,也就是没有引用任何哈希表。
和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:
func equal(x, y map[string]int) bool {
if len(x) != len(y) {
return false
}
for k, xv := range x {
if yv, ok := y[k]; !ok || yv != xv {
return false
}
}
return true
}
Map 的并发安全性
在多个 Goroutine 中使用 Map 时,我们需要注意 Map 的并发安全性。多个 Goroutine 对同一个 Map 进行读写操作时,可能会导致竞争条件和数据竞争等问题。为了解决这些问题,Go 语言提供了 sync 包中的 Map 类型。sync.Map 类型可以安全地在多个 Goroutine 中使用。
一般使用读写锁 sync.RWMutex 来保证安全。
Map 的底层原理
Map是一种通过key来获取value的一个数据结构,其底层存储方式为数组。
通过key进行hash运算(可以简单理解为把key转化为一个整形数字)然后对数组的长度取余,得到key存储在数组的哪个下标位置。
hash 冲突
开放定址法:在剩余未占用,挑选一个位置。
挑选位置的方法如下:
- 线性探测:插入时向后循环插入;查找时向后循环查找
- 拉链法:冲突位置上变为链表
开放定址(线性探测)和拉链的优缺点
-
由上面可以看出拉链法比线性探测处理简单
-
线性探测查找是会被拉链法会更消耗时间
-
线性探测会更加容易导致扩容,而拉链不会
-
拉链存储了指针,所以空间上会比线性探测占用多一点
-
拉链是动态申请存储空间的,所以更适合链长不确定的
go 语言的Map原理
而在 Go 语言中的 Map 是使用拉链法实现的哈希表。
底层结构体是hmap,hmap里维护着若干个bucket数组 (即桶数组)。
- tophash 用来快速查找key值是否在该 bucket 中
- 内存对齐 k,v
- 当一个bucket满时,通过overfolw指针链接到下一个bucket。
当往map中存储一个kv对时,通过k获取hash值,hash值的低八位和bucket数组长度取余,定位到在数组中的那个下标,hash值的高八位存储在bucket中的tophash中,用来快速判断key是否存在,key和value的具体值则通过指针运算存储。
扩容
- 装载因子已经超过 6.5;
- 哈希使用了太多溢出桶;
总结
通过对 Map 基本原理的了解,可以简单总结:
- Go 中的 Map 是使用拉链法实现的哈希表。
- Map 对应 hmap 结构体,每个桶对应 bmap 结构体。 每个 bmap 结构体包含三个大小固定为 8 的数组,以及用于挂载溢出桶的指针。
- 核心操作是查找,首先生成键的哈希值。
- 如果 Map 处于扩容状态,使用旧桶计算索引,在旧桶中查找;找不到时,利用新桶数组计算索引,在新桶中查找。
- 如果不处于扩容状态,直接利用桶数组计算索引,在桶数组中查找元素。
- 在添加元素之前,需要判断是否需要扩容。 当装载因子大于 6.5 时,需要执行翻倍扩容;当溢出桶数量过多时,只需等量扩容整理。
- 需要扩容时,采用渐进式驱逐的方式。 然后将元素放入适当的位置。
- 删除元素与查找元素非常相似,只是当找到对应键时,将相关信息删除而不是返回。