Go Map

1. 什么是 Map

Map 是 Go 的内置哈希表实现,存储键值对,零值为 nil

  • 引用类型,必须用 make 或字面量初始化后才能使用
  • nil map 可以读取(返回零值),但写入会 panic
  • 无序,遍历顺序每次运行都可能不同
  • key 必须支持 == 比较(不能是 slice、map、func)

2. 创建 Map

2.1 字面量

m := map[string]int{
    "apple":  5,
    "banana": 3,
}

2.2 make 函数

m := make(map[string]int)       // 空 map
m := make(map[string]int, 100)  // 预分配 100 个桶,减少扩容

2.3 nil map

var m map[string]int  // nil map
m["key"] = 1          // panic: assignment to entry in nil map
_ = m["key"]          // 合法,返回零值 0

3. 基本操作

3.1 增 / 改

m := map[string]int{}
m["apple"] = 5   // 新增
m["apple"] = 10  // 修改(同 key 直接覆盖)

3.2 读取

v := m["apple"]

// 判断 key 是否存在(comma ok 惯用法)
v, ok := m["apple"]
if ok {
    fmt.Println("存在,值为", v)
} else {
    fmt.Println("不存在")
}

不存在的 key 返回值类型的零值,不会报错。

3.3 删除

delete(m, "apple")       // key 不存在时无操作,不会 panic
delete(m, "nonexistent") // 安全

3.4 遍历

for k, v := range m {
    fmt.Println(k, v)
}

// 只遍历 key
for k := range m {
    fmt.Println(k)
}

遍历顺序是随机的。如果需要有序遍历,先提取 key 到切片并排序。


4. Map 作为集合(Set)

Go 没有内置 Set 类型,用 map[T]struct{} 实现:

set := map[string]struct{}{}

// 添加
set["apple"] = struct{}{}

// 判断存在
if _, ok := set["apple"]; ok {
    fmt.Println("存在")
}

// 删除
delete(set, "apple")

struct{} 作为 value 是因为它是零大小类型,不占额外内存。


5. Map 的内部结构

5.1 核心结构(runtime/map.go)

type hmap struct {
    count     int     // 元素个数,len(m) 返回的值
    B         uint8   // 桶数量 = 2^B
    overflow  uint16  // 溢出桶的近似数量
    buckets   unsafe.Pointer  // 桶数组
    oldbuckets unsafe.Pointer // 扩容时的旧桶
    // ...
}

type bmap struct {
    tophash [8]uint8  // 每个 key 的 hash 高 8 位
    // 后续内存布局(编译器生成):
    // keys   [8]keytype
    // values [8]valuetype
    // overflow *bmap
}

每个桶存储 8 个键值对,超出则链接溢出桶。

5.2 访问流程

key → hash → 低 B 位定位桶 → 高 8 位 tophash 快速筛选 → 逐个比较 key → 返回 value

5.3 负载因子与扩容

条件触发类型
负载因子 > 6.5(count / 2^B)翻倍扩容双倍扩容
溢出桶过多等量整理等量扩容(不增大桶数,合并稀疏溢出桶)

等量扩容发生在大量删除后,溢出桶多但元素少,整理以减少内存碎片。

扩容是渐进式的:每次插入/删除操作迁移少量旧桶,不会一次性完成。


6. 并发安全

6.1 Map 不是并发安全的

m := map[string]int{}

// 并发读写 → fatal error: concurrent map writes
go func() { m["a"] = 1 }()
go func() { m["b"] = 2 }()

Go 运行时检测到并发写会直接 fatal,不是 panic,无法 recover。

6.2 方案一:sync.Mutex / sync.RWMutex

type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func (s *SafeMap) Get(key string) (int, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    v, ok := s.m[key]
    return v, ok
}

func (s *SafeMap) Set(key string, val int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = val
}

读多写少时用 RWMutex,读写都频繁时用 Mutex(锁竞争开销更低)。

6.3 方案二:sync.Map

var m sync.Map

m.Store("key", 1)               // 写
v, ok := m.Load("key")          // 读
v, ok := m.LoadOrStore("key", 2) // 不存在则写入
m.Delete("key")                  // 删除
m.Range(func(k, v any) bool {    // 遍历
    fmt.Println(k, v)
    return true  // 返回 false 中止
})

适用场景(官方注释):

  1. key 只写一次,读很多次(缓存场景)
  2. 多个 goroutine 读写不重叠的 key 集合(分片读写)

不推荐作为通用并发 map,大多数场景下 Mutex + map 性能更优。


7. 常见陷阱

7.1 不可取地址

m := map[string]User{}
m["a"] = User{Name: "Tom"}

m["a"].Name = "Jerry"  // 编译错误:cannot assign to struct field m["a"].Name

map 的 value 不可寻址(可能因扩容随时移动)。解决:

// 方法一:整体替换
u := m["a"]
u.Name = "Jerry"
m["a"] = u

// 方法二:存储指针
m2 := map[string]*User{}
m2["a"] = &User{Name: "Tom"}
m2["a"].Name = "Jerry"  // 合法

7.2 遍历中删除

m := map[string]int{"a": 1, "b": 2, "c": 3}

// 合法,Go 允许遍历中删除
for k, v := range m {
    if v == 2 {
        delete(m, k)
    }
}

遍历中删除是安全的。但遍历中新增的行为是未定义的(结果不确定)。

7.3 nil map 的读写

var m map[string]int

_ = m["key"]   // 合法,返回 0
m["key"] = 1   // panic
delete(m, "key") // panic(Go 1.x 行为,delete nil map 在某些版本会 panic)
len(m)           // 合法,返回 0

7.4 map 不支持 == 比较

m1 := map[string]int{"a": 1}
m2 := map[string]int{"a": 1}

m1 == m2  // 编译错误
m1 == nil // 唯一允许的比较

如需比较两个 map 是否相等,需自行遍历:

func mapsEqual(a, b map[string]int) bool {
    if len(a) != len(b) {
        return false
    }
    for k, v := range a {
        if bv, ok := b[k]; !ok || bv != v {
            return false
        }
    }
    return true
}

8. 性能建议

建议说明
预分配容量make(map[K]V, n) 减少扩容次数
避免小对象作 value存指针减少拷贝开销,但会增加 GC 压力
key 类型影响性能短整型最快,字符串次之,结构体较慢
分片锁高并发下按 key 哈希分片,每片独立加锁
不要滥用 sync.MapMutex + map 在大多数场景更快

9. Go 1.21+ 泛型增强

9.1 maps 包(标准库)

import "maps"

m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 3, "c": 4}

cloned := maps.Clone(m1)          // 浅拷贝
maps.Copy(m1, m2)                 // m2 覆盖到 m1 → {"a":1,"b":3,"c":4}
equal := maps.Equal(m1, m2)       // 是否相等
keys := maps.Keys(m1)             // 迭代器,遍历所有 key
vals := maps.Values(m1)           // 迭代器,遍历所有 value
filtered := maps.DeleteFunc(m1, func(k string, v int) bool {
    return v < 2                   // 删除值小于 2 的 entry
})

10. 常用操作速查表

操作代码
创建m := map[K]V{}make(map[K]V)
预分配make(map[K]V, n)
写入m[k] = v
读取v := m[k]
判断存在v, ok := m[k]
删除delete(m, k)
长度len(m)
遍历for k, v := range m {}
集合set := map[T]struct{}{}
并发安全sync.Mutex + mapsync.Map
比较maps.Equal(m1, m2)(Go 1.21+)

11. 总结

  • Map 是引用类型,零值为 nil,nil map 可读不可写
  • key 必须可比较(不能是 slice / map / func)
  • 内部是哈希表,每桶 8 个键值对,负载因子 > 6.5 触发双倍扩容
  • 不是并发安全的,并发写会直接 fatal
  • value 不可取地址,修改 struct 值需先取出再整体写回
  • sync.Map 只适用于特定场景,通用场景用 Mutex + map

曼波