Go Map
1. 什么是 Map
Map 是 Go 的内置哈希表实现,存储键值对,零值为 nil。
- 引用类型,必须用
make或字面量初始化后才能使用 nilmap 可以读取(返回零值),但写入会 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 中止
})
适用场景(官方注释):
- key 只写一次,读很多次(缓存场景)
- 多个 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.Map | Mutex + 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 + map 或 sync.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