本文基于小徐先生的编程世界学习map
1.基本用法
1.核心特征
(1)存储基于 key-value 对映射的模式;
(2)基于 key 维度实现存储数据的去重;
(3)读、写、删操作控制,时间复杂度 O(1).
2.初始化方法
myMap1 := make(map[int]int,2)
myMap2 := make(map[int]int)
myMap3 :=map[int]int{
1:2,
3:4,
}
3.key 的类型要求
map 中,key 的数据类型必须为可比较的类型,slice、map、func不可比较
4.读
读 map 分为下面两种方式:
v1 := myMap[10]
第一种方式是直接读,倘若 key 存在,则获取到对应的 val,倘若 key 不存在或者 map 未初始化,会返回 val 类型的零值作为兜底.
v2,ok := myMap[10]
第二种方式是读的同时添加一个 bool 类型的 flag 标识是否读取成功. 倘若 ok == false,说明读取失败, key 不存在,或者 map 未初始化.
此处同一种语法能够实现不同返回值类型的适配,是由于代码在汇编时,会根据返回参数类型的区别,映射到不同的实现方法.
5.写
myMap[5] = 6
写操作的语法如上. 须注意的一点是,倘若 map 未初始化,直接执行写操作会导致 panic
6.删除
delete(myMap,5)
执行 delete 方法时,倘若 key 存在,则会从 map 中将对应的 key-value 对删除;倘若 key 不存在或 map 未初始化,则方法直接结束,不会产生显式提示.
7. 遍历
遍历分为下面两种方式:
for k,v := range myMap{
// ...
}
基于 k,v 依次承接 map 中的 key-value 对;
for k := range myMap{
// ...
}
基于 k 依次承接 map 中的 key,不关注 val 的取值.
需要注意的是,在执行 map 遍历操作时,获取的 key-value 对并没有一个固定的顺序,因此前后两次遍历顺序可能存在差异.
8.并发冲突
map 不是并发安全的数据结构,倘若存在并发读写行为,会抛出 fatal error.
具体规则是:
(1)并发读没有问题;
(2)并发读写中的“写”是广义上的,包含写入、更新、删除等操作;
(3)读的时候发现其他 goroutine 在并发写,抛出 fatal error;
(4)写的时候发现其他 goroutine 在并发写,抛出 fatal error.
并发安全的 map 通常可以使用以下几种方法:
- sync.Map:Go 语言标准库提供的并发安全的 map 实现,适用于读多写少的场景。
- 使用互斥锁(sync.Mutex):手动在对 map 进行读写操作时加锁。
- 使用读写锁(sync.RWMutex):在读多写少的场景下,使用读写锁可以提高并发性能。
2.核心原理
map 又称为 hash map,在算法上基于== hash表== 实现 key 的映射和寻址;在数据结构上基于桶数组实现 key-value 对的存储.
hash是一种将任意长度的输入压缩到某一固定长度的输出摘要的过程,由于这种转换属于压缩映射,输入空间远大于输出空间,因此不同输入可能会映射成相同的输出结果.
hash的特点:
(1)hash 的可重入性:相同的 key,必然产生相同的 hash 值;
(2)hash 的离散性:只要两个 key 不相同,不论其相似度的高低,产生的 hash 值会在整个输出域内均匀地离散化;
(3)hash 的单向性:企图通过 hash 值反向映射回 key 是无迹可寻的.
(4)hash 冲突:由于输入域(key)无穷大,输出域(hash 值)有限,因此必然存在不同 key 映射到相同 hash 值的情况,称之为 hash 冲突.
3.数据结构
hmap
type hmap struct {
count int //当前保存元素个数
flags uint8
B uint8 //bucket数组大小->2^B
noverflow uint16 //map 中溢出桶的数量;
hash0 uint32 //hash 随机因子,生成 key 的 hash 值时会使用到
buckets unsafe.Pointer //bucket数组,长度为2^B
oldbuckets unsafe.Pointer //老旧的bucket数组,在扩容时出现
nevacuate uintptr // 扩容时的进度标识,index 小于 nevacuate 的桶都已经由老桶转移到新桶中;
extra *mapextra //预申请的溢出桶.
}
bmap
bucket数据结构:
const bucketCnt = 8
type bmap struct {
tophash [bucketCnt]uint8
data []byte//k0/..../k7/val1/.../val7每个bucket可以存储8个kv对
overflow *bmap
}
- tophash:长度为8的整形数组,存储hash值的高位
- data:存放kv数据
- overflow:指向下一个bucket将所有冲突的键连接
在代码层面只展示了 tophash 部分,但由于 tophash、key 和 val 的数据长度固定,因此可以通过内存地址偏移的方式寻找到后续的 key 数组、val 数组以及溢出桶指针
4.构造方法
1.makemap
func makemap(t *maptype, hint int, h *hmap) *hmap {
//(1)hint 为 map 拟分配的容量;在分配前,会提前对拟分配的内存大小进行判断,
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
//倘若超限,会将 hint 置为零;
if overflow || mem > maxAlloc {
hint = 0
}
//(2)通过 new 方法初始化 hmap;
if h == nil {
h = new(hmap)
}
//(3)调用 fastrand,构造 hash 因子:hmap.hash0;
h.hash0 = fastrand()
//致上基于 log2(B) >= hint 的思路,计算桶数组的容量 B;
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
//(5)调用 makeBucketArray 方法,初始化桶数组 hmap.buckets;
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
(6)倘若 map 容量较大,会提前申请一批溢出桶 hmap.extra.
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return
}
2.overLoadFactor
(1)倘若 map 预分配容量小于等于 8,B 取 0,桶的个数为 1;
(2)保证 map 预分配容量小于等于桶数组长度 * 6.5.
const loadFactorNum = 13
const loadFactorDen = 2
const goarch.PtrSize = 8
const bucketCnt = 8
func overLoadFactor(count int, B uint8) bool {
return count > bucketCnt && uintptr(count) > 13*(bucketShift(B)/2)
}
func bucketShift(b uint8) uintptr {

最低0.47元/天 解锁文章
822

被折叠的 条评论
为什么被折叠?



