Files
cs-note/hzh/GolangStar/Go语言基础/Go语言Map.md
T

169 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, go基础语法, map, 哈希表]
create time: 2026-06-07 15:00
---
# Go 语言 Map
## 概述
Map 是 Go 的内置哈希表,提供 O(1) 平均复杂度的键值对操作。本文覆盖创建、增删改查、遍历时序随机性、并发安全等核心要点。
## 正文
### 为什么 Go 的 Map 遍历顺序是随机的?
这不是 bug,而是**有意设计**。Go 的 map 在每次遍历时会随机打乱起始位置,目的是**防止程序员依赖遍历顺序**——而依赖未定义行为正是很多 bug 的源头。
> [!warning] ⚠️ 记住这条规则
> **永远不要依赖 map 的遍历顺序。** 如果需要有序输出,先收集 key 再排序。
### 创建与初始化
```go
// 方式一:make(空 map)
m := make(map[string]int)
// 方式二:字面量(带初始值)
scores := map[string]int{
"张三": 95,
"李四": 88,
}
// 方式三:var 声明(零值为 nil,需 make 后才能用)
var prices map[string]float64
prices = make(map[string]float64)
```
> [!note] 📝 nil map 不能写入
> `var m map[string]int` 创建的 map 是 `nil`。对它做 `m["a"] = 1` 会 panic。必须先 `make`。
### 基本操作
```go
// 写入 / 更新
m["apple"] = 5.5
// 读取(不存在的 key 返回零值)
price := m["apple"] // float64 → 0.0(如果 key 不存在)
// 判断 key 是否存在
price, exists := m["apple"]
if exists {
fmt.Println(price)
} else {
fmt.Println("不存在")
}
// 删除
delete(m, "apple")
// 获取元素个数
fmt.Println(len(m))
```
> [!tip] 💡 单返回值惯用法
> 判断 key 是否存在还有一种更简洁的写法:
> ```go
> if price, ok := m["apple"]; ok {
> // key 存在
> }
> ```
> `ok` 是 Go 中约定俗成的布尔变量名,表示"是否成功"。
### 嵌套 Map
Map 的值可以是另一个 Map:
```go
studentScores := map[string]map[string]int{
"张三": {"数学": 95, "英语": 88},
"李四": {"数学": 90, "英语": 85},
}
// 获取张三的英语成绩
score := studentScores["张三"]["英语"]
```
> [!warning] ⚠️ 需要先初始化内层 map
> ```go
> studentScores["王五"] = make(map[string]int) // 必须先 make
> studentScores["王五"]["数学"] = 92 // 然后才能写入
> ```
### Map 作为函数参数
Map 是**引用类型**——传入函数后修改会影响原始 map:
```go
func addScore(scores map[string]int, name string, score int) {
scores[name] = score // 直接修改原 map
}
m := map[string]int{"张三": 95}
addScore(m, "李四", 88)
fmt.Println(m) // map[张三:95 李四:88]
```
### 性能优化:预分配容量
如果知道 map 的大致大小,传 `cap` 可以减少扩容开销:
```go
// 预计约 1000 个元素
m := make(map[string]int, 1000)
```
> [!info] ℹ️ 注意
> `make(map[T]U, 1000)` 中的第二个参数是**容量提示**(不是强制限制)。map 仍会自动扩容,但预分配可以减少重新分配的次数。
### 并发安全
**普通 map 不是并发安全的**。多个 goroutine 同时写会 panic:
```
fatal error: concurrent map writes
```
#### 方案一:sync.Mutex
```go
var mu sync.Mutex
m := make(map[string]int)
mu.Lock()
m["key"] = 1
mu.Unlock()
mu.Lock()
val := m["key"]
mu.Unlock()
```
#### 方案二:sync.Map
适用于读多写少或 key 集合固定的场景:
```go
var sm sync.Map
sm.Store("key", 42) // 写入
val, ok := sm.Load("key") // 读取 → (interface{}, bool)
sm.Delete("key") // 删除
```
> [!note] 📝 sync.Map vs Mutex+map
> - `sync.Map` 适合 key 集固定、读远多于写的场景
> - 写频繁时,`sync.Mutex + map` 通常性能更好
> - 具体选择取决于你的读写比例
### 常见陷阱总结
| 陷阱 | 说明 | 修复 |
|------|------|------|
| nil map 写入 | `var m map[K]V` 后直接赋值会 panic | 先用 `make` |
| 不存在的 key 返回零值 | `m["missing"]` 返回类型的零值 | 用 `v, ok := m[k]` 判断 |
| 遍历顺序随机 | 每次运行顺序不同 | 需要有序则先排序 key |
| 并发写 panic | 多 goroutine 同时写 | 加锁或用 `sync.Map` |
| 不能取元素地址 | `&m["key"]` 编译错误 | 无法直接取 map 值的地址 |