Go 语言 Map 详解:声明、使用与底层原理
1. Map 基础概念
Map 在 Go 中常被称为字典或关联数组,其核心特性包括:
- 数据以 key-value 对的形式存储
- key 具有唯一性,可用于去重
- 增删改查操作的时间复杂度均为 O(1)
2. Map 声明
2.1 语法格式
var map变量名 map[key类型]value类型
key 类型要求:
- 支持:bool、数字、string、指针、channel、以及包含这些类型的接口、结构体、数组
- 不支持:slice、map、function(无法使用 == 比较)
value 类型:同样支持上述类型,常用数字、string、map、struct
2.2 声明示例
var m1 map[string]string
var m2 map[string]int
var m3 map[int]string
var m4 map[string]map[string]string
注意:声明后必须通过 make 初始化才能使用,否则进行写操作会导致 panic。
2.3 初始化与赋值
func main() {
var data map[string]string
fmt.Println(data) // map[]
// 必须 make 后使用
data = make(map[string]string, 10)
data["name"] = "张三"
data["id"] = "001"
println(data["name"])
}
关键点:
- key 不可重复,重复时以最后一次赋值为准
- value 可以相同
- key-value 顺序是无序的
3. Map 使用方式
3.1 三种初始化方式
// 方式一:先声明再 make
var m1 map[string]int
m1 = make(map[string]int, 5)
// 方式二:声明时直接 make
m2 := make(map[string]int)
m2["a"] = 100
// 方式三:声明时初始化
m3 := map[string]string{
"k1": "v1",
"k2": "v2", // 逗号不能省略
}
3.2 复杂 map 实例
students := make(map[string]map[string]string)
students["001"] = make(map[string]string, 2)
students["001"]["name"] = "张三"
students["001"]["gender"] = "男"
students["002"] = make(map[string]string, 2)
students["002"]["name"] = "李四"
students["002"]["gender"] = "女"
fmt.Println(students)
4. CRUD 操作
// 创建和修改
cities := make(map[string]string)
cities["bj"] = "北京"
cities["sh"] = "上海"
cities["sz"] = "深圳"
cities["bj"] = "重庆" // 修改
// 删除
delete(cities, "bj") // 存在则删除
delete(cities, "gz") // 不存在不报错
// 查找
if val, ok := cities["sh"]; ok {
fmt.Printf("存在,值为:%s\n", val)
}
// 清空所有 key
// 方法 1:遍历删除
for k := range cities {
delete(cities, k)
}
// 方法 2:重新 make(推荐)
cities = make(map[string]string)
5. 遍历操作
// 简单 map 遍历
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Printf("key=%s, value=%d\n", k, v)
}
// 嵌套 map 遍历
students := map[string]map[string]string{
"001": {"name": "张三", "gender": "男"},
"002": {"name": "赵敏", "gender": "女"},
}
for id, info := range students {
fmt.Printf("学号:%s\n", id)
for field, val := range info {
fmt.Printf("\t%s=%s\n", field, val)
}
}
// 获取 map 长度
fmt.Println("学生数量:", len(students))
6. 并发安全性
Go 的 map 不是并发安全的。具体规则:
- 多个 goroutine 并发读取是安全的
- 存在并发写入(包括增、改、删)时会触发 fatal error
- 读取时发现其他 goroutine 在写入,抛出:
concurrent map read and map write - 写入时发现并发写入,抛出:
concurrent map writes
这类 fatal error 比 panic 更严重,无法用 recover 恢复。
7. 底层实现原理
7.1 核心结构
Go 的 map 基于哈希表实现,核心数据结构包括:
- hmap:map 的元数据结构
- bmap:桶结构,每个桶存储 8 个 key-value 对
- mapextra:溢出桶管理
7.2 hmap 结构
type hmap struct {
count int // key-value 总数
flags uint8 // 状态标记(如是否被并发写)
B uint8 // 桶数组长度指数(2^B)
noverflow uint16 // 溢出桶数量
hash0 uint32 // 随机哈希因子
buckets unsafe.Pointer // 桶数组
oldbuckets unsafe.Pointer // 扩容时的旧桶数组
nevacuate uintptr // 扩容进度标记
extra *mapextra // 预分配的溢出桶
}
7.3 bmap 桶结构
const bucketCnt = 8
type bmap struct {
tophash [bucketCnt]uint8 // 存储 key 的高 8 位哈希值
keys [bucketCnt]T // key 数组
values [bucketCnt]T // value 数组
overflow uint8 // 溢出桶指针
}
7.4 哈希冲突解决
当不同 key 映射到同一桶时:
- 先尝试在当前桶的空位插入(开放寻址法)
- 桶满时,通过溢出桶指针形成链表(拉链法)
- 每个桶最多 8 个 key-value 对
7.5 扩容机制
触发条件:
- 增量扩容:key-value 总数超过 6.5 * 桶数组长度
- 等量扩容:溢出桶数量超过 2^B 个(B 为桶长度指数)
扩容方式:
- 增量扩容:桶数组长度翻倍
- 等量扩容:桶长度保持不变,重新整理数据
- 采用渐进式迁移,每次写入或删除操作时迁移两组桶,避免性能抖动
8. 读写流程
8.1 读取流程
- 计算 key 的哈希值
- 取模确定桶位置
- 遍历桶链表查找 key
- 找到则返回 value,否则返回零值
8.2 写入流程
- 计算 key 的哈希值
- 确定目标桶,若需扩容则迁移旧桶
- 遍历查找 key,存在则更新,不存在则插入
- 插入前检查是否需要触发扩容
- 更新 count 和标记
9. 删除流程
- 计算 key 的哈希值并定位桶
- 若处于扩容则协助迁移
- 遍历桶链表找到 key
- 删除 key-value 对,并将 tophash 标记为 emptyOne
- 清理相邻的 emptyOne 标记为 emptyRest(表示后续均为空)
