Go语言核心知识点底层原理教程【Map的底层原理】

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的顺序是随机的,这是有意为之的设计
  • 预分配容量可以提高性能

14. 参考资料


相关推荐
后端小张2 小时前
【AI 学习】LangChain框架深度解析:从核心组件到企业级应用实战
java·人工智能·学习·langchain·tensorflow·gpt-3·ai编程
未来之窗软件服务2 小时前
幽冥大陆(六十二) 多数据库交叉链接系统Go语言—东方仙盟筑基期
数据库·人工智能·oracle·golang·数据库集群·仙盟创梦ide·东方仙盟
天天摸鱼的java工程师2 小时前
后端密码存储优化:BCrypt 与 Argon2 加密方案对比
java·后端
雨中飘荡的记忆2 小时前
Vavr:让Java拥抱函数式编程的利器
java
沈千秋.2 小时前
xss.pwnfunction.com闯关(1~6)
java·前端·xss
关于不上作者榜就原神启动那件事2 小时前
Spring Data Redis 使用详解
java·redis·spring
invicinble2 小时前
java集合类(二)--map
java·开发语言·python
Mr-Wanter2 小时前
搭建局域网时间同步服务器
java·运维·服务器
代码笔耕2 小时前
我们这样设计消息中心,解决了业务反复折腾的顽疾
java·后端·架构