Map的底层原理
1. Map概述
Map是Go语言中的哈希表实现,提供了键值对的存储和快速查找功能。
1.1 Map的特性
- 无序性:遍历map的顺序是随机的
- 引用类型:map是引用类型,传递时不会复制数据
- 非线程安全:并发读写需要加锁
- 动态扩容:自动扩容以保持性能
2. Map的底层结构
2.1 核心数据结构
go
// src/runtime/map.go
// map的主结构
type hmap struct {
count int // 元素个数,len()返回此值
flags uint8 // 状态标志
B uint8 // buckets数组的长度的对数,即len(buckets) == 2^B
noverflow uint16 // 溢出桶的数量
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // 指向buckets数组,大小为2^B
oldbuckets unsafe.Pointer // 扩容时指向旧的buckets数组
nevacuate uintptr // 扩容进度,小于此值的buckets已迁移
extra *mapextra // 可选字段
}
// bucket结构(每个bucket可存储8个键值对)
type bmap struct {
// tophash存储每个key的hash值的高8位
tophash [bucketCnt]uint8
// 接下来是8个key,8个value,以及一个overflow指针
// 但这些字段在编译时动态生成,不在结构体定义中
}
const (
bucketCntBits = 3
bucketCnt = 1 << bucketCntBits // 8
)
2.2 内存布局
hmap结构
┌─────────────────────────────────────┐
│ count: 5 │
│ B: 2 (4个buckets) │
│ buckets ────────────────────┐ │
│ oldbuckets: nil │ │
│ hash0: 0x12345678 │ │
└─────────────────────────────┼───────┘
│
▼
Buckets Array (2^B = 4)
┌─────────┬─────────┬─────────┬─────────┐
│ bmap 0 │ bmap 1 │ bmap 2 │ bmap 3 │
└────┬────┴─────────┴─────────┴─────────┘
│
▼
Single Bucket (bmap)
┌──────────────────────────────┐
│ tophash [8]uint8 │
├──────────────────────────────┤
│ key0, key1, ..., key7 │
├──────────────────────────────┤
│ value0, value1, ..., value7 │
├──────────────────────────────┤
│ overflow *bmap (可选) │
└──────────────────────────────┘
2.3 为什么key和value分开存储?
go
// 不是这样存储:
// [key0][value0][key1][value1]...
// 而是这样:
// [key0][key1]...[key7][value0][value1]...[value7]
// 原因:内存对齐
// 例如:map[int64]int8
// 分开存储:[8][8][8][8][8][8][8][8][1][1][1][1][1][1][1][1] = 64字节
// 交替存储:[8][1][padding:7][8][1][padding:7]... = 128字节
3. Map的创建
3.1 字面量创建
go
m := map[string]int{
"a": 1,
"b": 2,
}
// 编译器会转换为:
m := make(map[string]int)
m["a"] = 1
m["b"] = 2
3.2 make函数创建
go
// 不指定容量
m1 := make(map[string]int)
// 指定初始容量
m2 := make(map[string]int, 100)
// 底层实现
// src/runtime/map.go
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 计算需要的bucket数量
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
// 分配hmap结构
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
// 分配buckets数组
h.B = B
if h.B != 0 {
var nextOverflow *bmap
h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
if nextOverflow != nil {
h.extra = new(mapextra)
h.extra.nextOverflow = nextOverflow
}
}
return h
}
4. Map的访问
4.1 查找过程
go
m := map[string]int{"hello": 1}
v := m["hello"]
// 底层实现
// src/runtime/map.go
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// 1. 计算key的hash值
hash := t.hasher(key, uintptr(h.hash0))
// 2. 计算bucket索引
m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
// 3. 获取tophash(hash的高8位)
top := tophash(hash)
// 4. 在bucket中查找
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.key.equal(key, k) {
// 找到了,返回value
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
return v
}
}
}
// 未找到,返回零值
return unsafe.Pointer(&zeroVal[0])
}
4.2 查找流程图
1. 计算hash值
hash = hash(key)
2. 计算bucket索引
index = hash & (2^B - 1)
3. 获取tophash
top = hash >> 56 (高8位)
4. 在bucket中查找
┌─────────────────┐
│ tophash[0..7] │ ← 先比较tophash
├─────────────────┤
│ keys[0..7] │ ← tophash匹配后比较完整key
├─────────────────┤
│ values[0..7] │ ← 找到key后返回value
├─────────────────┤
│ overflow │ ← 如果当前bucket未找到,查找溢出桶
└─────────────────┘
4.3 两种访问方式
go
// 方式1:直接访问
v := m["key"] // 如果key不存在,返回零值
// 方式2:检查是否存在
v, ok := m["key"]
if ok {
// key存在
} else {
// key不存在
}
// 底层使用不同的函数
// mapaccess1: 只返回value
// mapaccess2: 返回value和bool
5. Map的插入和更新
5.1 插入过程
go
m["key"] = value
// 底层实现
// src/runtime/map.go
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// 1. 计算hash值
hash := t.hasher(key, uintptr(h.hash0))
// 2. 设置写标志
h.flags ^= hashWriting
// 3. 如果buckets为空,分配
if h.buckets == nil {
h.buckets = newobject(t.bucket)
}
again:
// 4. 计算bucket索引
bucket := hash & bucketMask(h.B)
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
top := tophash(hash)
var inserti *uint8
var insertk unsafe.Pointer
var elem unsafe.Pointer
// 5. 查找key或空位
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if !t.key.equal(key, k) {
continue
}
// 找到已存在的key,更新value
elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
goto done
}
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// 6. 检查是否需要扩容
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again
}
// 7. 如果没有空位,分配溢出桶
if inserti == nil {
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
elem = add(insertk, bucketCnt*uintptr(t.keysize))
}
// 8. 插入key和tophash
t.key.copy(insertk, key)
*inserti = top
h.count++
done:
// 9. 清除写标志
h.flags &^= hashWriting
return elem
}
6. Map的删除
6.1 删除操作
go
delete(m, "key")
// 底层实现
// src/runtime/map.go
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
// 1. 计算hash值
hash := t.hasher(key, uintptr(h.hash0))
// 2. 设置写标志
h.flags ^= hashWriting
// 3. 查找key
bucket := hash & bucketMask(h.B)
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
top := tophash(hash)
search:
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top {
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if !t.key.equal(key, k) {
continue
}
// 4. 找到key,清除key和value
t.key.clear(k)
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
t.elem.clear(e)
// 5. 标记为空
b.tophash[i] = emptyOne
h.count--
break search
}
}
// 6. 清除写标志
h.flags &^= hashWriting
}
6.2 删除标记
go
const (
emptyRest = 0 // 此位置为空,且后面都是空的
emptyOne = 1 // 此位置为空
evacuatedX = 2 // 已迁移到新bucket的前半部分
evacuatedY = 3 // 已迁移到新bucket的后半部分
evacuatedEmpty = 4 // 已迁移且为空
minTopHash = 5 // 正常的tophash最小值
)
7. Map的扩容
7.1 扩容触发条件
go
// 条件1:负载因子超过6.5
// loadFactor = count / (2^B * 8) > 6.5
// 条件2:溢出桶过多
// noverflow >= 2^min(B, 15)
7.2 扩容类型
1. 增量扩容(翻倍扩容)
go
// 当负载因子过高时
// B' = B + 1
// buckets数量翻倍
旧buckets (2^B) 新buckets (2^(B+1))
┌────┬────┬────┬────┐ ┌────┬────┬────┬────┬────┬────┬────┬────┐
│ 0 │ 1 │ 2 │ 3 │ → │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │
└────┴────┴────┴────┘ └────┴────┴────┴────┴────┴────┴────┴────┘
2. 等量扩容(整理扩容)
go
// 当溢出桶过多但负载因子不高时
// B' = B
// buckets数量不变,但整理溢出桶
旧buckets + 溢出桶 新buckets (整理后)
┌────┐ ┌────┐
│ 0 │──→[overflow] │ 0 │
├────┤ ├────┤
│ 1 │──→[overflow] │ 1 │
├────┤ ├────┤
│ 2 │ │ 2 │
└────┘ └────┘
7.3 渐进式扩容
Go的map扩容是渐进式的,不会一次性完成:
go
// src/runtime/map.go
func hashGrow(t *maptype, h *hmap) {
// 1. 确定扩容类型
bigger := uint8(1)
if !overLoadFactor(h.count+1, h.B) {
bigger = 0 // 等量扩容
h.flags |= sameSizeGrow
}
// 2. 保存旧buckets
oldbuckets := h.buckets
// 3. 分配新buckets
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
// 4. 更新hmap
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0
// 实际的数据迁移在后续的访问和插入操作中逐步完成
}
// 每次访问或插入时,迁移1-2个bucket
func growWork(t *maptype, h *hmap, bucket uintptr) {
// 迁移当前bucket
evacuate(t, h, bucket&h.oldbucketmask())
// 再迁移一个bucket,加速扩容
if h.growing() {
evacuate(t, h, h.nevacuate)
}
}
7.4 数据迁移
go
// 迁移一个bucket
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
newbit := h.noldbuckets()
// 遍历旧bucket中的所有元素
for ; b != nil; b = b.overflow(t) {
for i := 0; i < bucketCnt; i++ {
top := b.tophash[i]
if isEmpty(top) {
continue
}
k := add(unsafe.Pointer(b), dataOffset+uintptr(i)*uintptr(t.keysize))
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(i)*uintptr(t.elemsize))
// 重新计算hash,确定新位置
hash := t.hasher(k, uintptr(h.hash0))
// 如果是翻倍扩容,元素可能分配到X或Y
// X: 新bucket的前半部分(索引不变)
// Y: 新bucket的后半部分(索引+2^B)
useY := hash&newbit != 0
// 插入到新bucket
// ...
}
}
// 标记旧bucket已迁移
h.nevacuate++
}
8. Map的遍历
8.1 遍历的随机性
go
m := map[string]int{"a": 1, "b": 2, "c": 3}
// 每次遍历的顺序可能不同
for k, v := range m {
fmt.Println(k, v)
}
// 底层实现:随机选择起始bucket
// src/runtime/map.go
func mapiterinit(t *maptype, h *hmap, it *hiter) {
it.t = t
it.h = h
// 随机选择起始位置
r := uintptr(fastrand())
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
// ...
}
8.2 遍历过程中的修改
go
// 遍历时可以删除元素
for k := range m {
if someCondition(k) {
delete(m, k) // 安全
}
}
// 遍历时可以修改value
for k, v := range m {
m[k] = v * 2 // 安全
}
// 遍历时添加元素
for k := range m {
m[k+"_new"] = 1 // 新元素可能被遍历到,也可能不会
}
9. Map的并发安全
9.1 并发问题
go
// 并发读写会panic
m := make(map[string]int)
go func() {
for {
m["key"] = 1 // 写
}
}()
go func() {
for {
_ = m["key"] // 读
}
}()
// fatal error: concurrent map read and map write
9.2 解决方案
方案1:使用互斥锁
go
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
v, ok := sm.m[key]
return v, ok
}
func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
方案2:使用sync.Map
go
var m sync.Map
// 存储
m.Store("key", "value")
// 读取
v, ok := m.Load("key")
// 删除
m.Delete("key")
// 读取或存储
actual, loaded := m.LoadOrStore("key", "value")
// 遍历
m.Range(func(key, value interface{}) bool {
fmt.Println(key, value)
return true // 返回false停止遍历
})
sync.Map适用场景:
- 读多写少
- 多个goroutine读写不同的key
10. 性能优化
10.1 预分配容量
go
// 不好:多次扩容
m := make(map[string]int)
for i := 0; i < 10000; i++ {
m[fmt.Sprintf("key%d", i)] = i
}
// 好:预分配容量
m := make(map[string]int, 10000)
for i := 0; i < 10000; i++ {
m[fmt.Sprintf("key%d", i)] = i
}
10.2 选择合适的key类型
go
// 不好:字符串key(需要计算hash)
m1 := make(map[string]int)
// 好:整数key(hash计算更快)
m2 := make(map[int]int)
// 不好:结构体key(需要比较所有字段)
type Key struct {
a, b, c string
}
m3 := make(map[Key]int)
// 好:使用指针或简单类型
m4 := make(map[*Key]int)
10.3 避免不必要的查找
go
// 不好:查找两次
if _, ok := m[key]; ok {
v := m[key]
process(v)
}
// 好:查找一次
if v, ok := m[key]; ok {
process(v)
}
11. Map的限制
11.1 key的要求
key必须是可比较的类型:
go
// 可以作为key的类型
map[int]string // ✓
map[string]int // ✓
map[struct{a int}]int // ✓
map[[3]int]int // ✓ 数组可以
// 不能作为key的类型
map[[]int]int // ✗ slice不可比较
map[map[string]int]int // ✗ map不可比较
map[func()]int // ✗ 函数不可比较
11.2 value的限制
value可以是任意类型,包括map本身:
go
// 嵌套map
m := make(map[string]map[string]int)
m["outer"] = make(map[string]int)
m["outer"]["inner"] = 1
12. 实战示例
12.1 实现LRU缓存
go
type LRUCache struct {
capacity int
cache map[int]*list.Element
list *list.List
}
type entry struct {
key int
value int
}
func Constructor(capacity int) LRUCache {
return LRUCache{
capacity: capacity,
cache: make(map[int]*list.Element),
list: list.New(),
}
}
func (c *LRUCache) Get(key int) int {
if elem, ok := c.cache[key]; ok {
c.list.MoveToFront(elem)
return elem.Value.(*entry).value
}
return -1
}
func (c *LRUCache) Put(key, value int) {
if elem, ok := c.cache[key]; ok {
c.list.MoveToFront(elem)
elem.Value.(*entry).value = value
return
}
if c.list.Len() >= c.capacity {
back := c.list.Back()
if back != nil {
c.list.Remove(back)
delete(c.cache, back.Value.(*entry).key)
}
}
elem := c.list.PushFront(&entry{key, value})
c.cache[key] = elem
}
12.2 实现计数器
go
func wordCount(text string) map[string]int {
counts := make(map[string]int)
words := strings.Fields(text)
for _, word := range words {
counts[word]++
}
return counts
}
12.3 实现集合操作
go
type Set map[string]struct{}
func NewSet() Set {
return make(Set)
}
func (s Set) Add(item string) {
s[item] = struct{}{}
}
func (s Set) Remove(item string) {
delete(s, item)
}
func (s Set) Contains(item string) bool {
_, ok := s[item]
return ok
}
func (s Set) Union(other Set) Set {
result := NewSet()
for k := range s {
result.Add(k)
}
for k := range other {
result.Add(k)
}
return result
}
func (s Set) Intersection(other Set) Set {
result := NewSet()
for k := range s {
if other.Contains(k) {
result.Add(k)
}
}
return result
}
13. 总结
- Map是基于哈希表实现的,使用链地址法解决冲突
- 每个bucket可存储8个键值对,超出时使用溢出桶
- Map的扩容是渐进式的,避免一次性大量数据迁移
- Map不是线程安全的,并发访问需要加锁
- 遍历Map的顺序是随机的,这是有意为之的设计
- 预分配容量可以提高性能