Go 语言完全指南 / 09 - Map:内部实现、并发安全、sync.Map
09 - Map
9.1 Map 基础
Map 是 Go 的内置关联数据类型(哈希表),存储键值对。
package main
import "fmt"
func main() {
// 创建方式一:字面量
scores := map[string]int{
"Alice": 95,
"Bob": 87,
"Carol": 92,
}
// 创建方式二:make
ages := make(map[string]int)
ages["Alice"] = 30
ages["Bob"] = 25
// 创建方式三:make 指定容量
data := make(map[string]int, 100)
fmt.Println(scores)
fmt.Println(ages)
fmt.Println(data)
// 读取
fmt.Println(scores["Alice"]) // 95
// 修改
scores["Alice"] = 98
// 添加
scores["Dave"] = 88
// 删除
delete(scores, "Bob")
// 长度
fmt.Println(len(scores))
// 遍历(顺序不确定)
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
}
9.2 Map 的特殊行为
检查键是否存在
func main() {
m := map[string]int{"a": 1, "b": 2}
// 逗号 ok 模式
v, ok := m["a"]
if ok {
fmt.Println("找到:", v)
}
// 不存在的键返回零值
fmt.Println(m["missing"]) // 0
// 区分"值为零"和"键不存在"
v2, ok2 := m["missing"]
fmt.Println(v2, ok2) // 0, false
// 常见用法
if _, exists := m["c"]; !exists {
fmt.Println("c 不存在")
}
}
nil Map
func main() {
var m map[string]int // nil map
// 读取 nil map(安全,返回零值)
fmt.Println(m["key"]) // 0
// ❌ 写入 nil map 会 panic
// m["key"] = 1 // panic: assignment to entry in nil map
// 必须先初始化
m = make(map[string]int)
m["key"] = 1 // OK
}
⚠️ 注意:nil map 可以读取但不能写入。使用前需要 make 初始化。
9.3 Map 的键类型
Map 的键必须是可比较的类型(支持 == 运算符)。
| 可作为键 ✅ | 不可作为键 ❌ |
|---|---|
| int, float64, string | slice |
| bool, rune, byte | map |
| pointer | func |
| array | struct(含不可比较字段) |
| struct(所有字段可比较) | — |
| interface(动态值可比较) | — |
func main() {
// 用结构体作为键
type Point struct{ X, Y int }
distances := map[Point]float64{
{0, 0}: 0,
{3, 4}: 5,
}
fmt.Println(distances[Point{3, 4}]) // 5
// 用数组(不是切片)作为键
pair := map[[2]int]string{
{1, 2}: "one-two",
{3, 4}: "three-four",
}
fmt.Println(pair[[2]int{1, 2}]) // one-two
// ❌ 切片不能作为键
// m := map[[]int]int{} // 编译错误
}
9.4 Map 的遍历
func main() {
m := map[string]int{
"a": 1, "b": 2, "c": 3, "d": 4, "e": 5,
}
// 遍历键值对
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
// 只遍历键
for k := range m {
fmt.Println(k)
}
// 只遍历值
for _, v := range m {
fmt.Println(v)
}
// 按顺序遍历(需要排序键)
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
}
⚠️ 注意:Go 的 map 遍历顺序是随机的。如果需要有序遍历,先对键排序。
9.5 Map 常见操作
package main
import (
"fmt"
"strings"
)
func main() {
// 统计词频
text := "the quick brown fox jumps over the lazy dog the fox"
wordCount := make(map[string]int)
for _, word := range strings.Fields(text) {
wordCount[word]++
}
fmt.Println(wordCount)
// map[the:3 quick:1 brown:1 fox:2 jumps:1 over:1 lazy:1 dog:1]
// 分组
words := []string{"apple", "banana", "avocado", "blueberry", "cherry", "apricot"}
groups := make(map[byte][]string)
for _, w := range words {
key := w[0]
groups[key] = append(groups[key], w)
}
fmt.Println(groups)
// map[a:[apple avocado] b:[banana blueberry] c:[cherry apricot]]
// 集合(用 map[T]bool 模拟)
set := make(map[string]bool)
set["a"] = true
set["b"] = true
set["a"] = true // 不会重复
fmt.Println(len(set)) // 2
fmt.Println(set["a"]) // true
fmt.Println(set["c"]) // false
// Map 作为缓存
cache := make(map[string]string)
getOrCompute := func(key string, compute func() string) string {
if val, ok := cache[key]; ok {
return val
}
val := compute()
cache[key] = val
return val
}
result := getOrCompute("db_url", func() string {
fmt.Println("计算一次...")
return "postgres://localhost:5432"
})
fmt.Println(result)
result2 := getOrCompute("db_url", func() string {
fmt.Println("不会执行")
return ""
})
fmt.Println(result2)
}
9.6 Map 并发安全
⚠️ 重要:Go 的内置 map 不是并发安全的。并发读写会导致 fatal error。
// ❌ 危险:并发读写 map
func unsafe() {
m := make(map[int]int)
go func() {
for i := 0; i < 1000; i++ {
m[i] = i // 写
}
}()
go func() {
for i := 0; i < 1000; i++ {
_ = m[i] // 读
}
}()
// fatal error: concurrent map read and map write
}
使用 sync.Mutex 保护
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{m: make(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, value int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = value
}
func (s *SafeMap) Delete(key string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.m, key)
}
func (s *SafeMap) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.m)
}
func main() {
sm := NewSafeMap()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(2)
go func(i int) {
defer wg.Done()
sm.Set(fmt.Sprintf("key-%d", i), i)
}(i)
go func(i int) {
defer wg.Done()
sm.Get(fmt.Sprintf("key-%d", i))
}(i)
}
wg.Wait()
fmt.Println("安全 map 长度:", sm.Len())
}
sync.Map
Go 标准库提供的并发安全 map:
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 存储
m.Store("name", "Alice")
m.Store("age", 30)
// 读取
v, ok := m.Load("name")
if ok {
fmt.Println("name:", v) // Alice
}
// LoadOrStore:如果存在返回已有值,否则存储并返回新值
actual, loaded := m.LoadOrStore("name", "Bob")
fmt.Println(actual, loaded) // Alice, true(已有值)
actual2, loaded2 := m.LoadOrStore("email", "[email protected]")
fmt.Println(actual2, loaded2) // [email protected], false(新值)
// 删除
m.Delete("age")
// 遍历
m.Range(func(key, value any) bool {
fmt.Printf("%v: %v\n", key, value)
return true // 返回 false 停止遍历
})
// LoadAndDelete:原子性读取并删除
v2, loaded3 := m.LoadAndDelete("name")
fmt.Println(v2, loaded3) // Alice, true
// LoadOrStore 的原子性保证
var wg sync.WaitGroup
var m2 sync.Map
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
m2.LoadOrStore("counter", 0)
}(i)
}
wg.Wait()
}
sync.Map 适用场景
| 场景 | 推荐方案 |
|---|---|
| 读多写少 | sync.Map(内部无锁读) |
| 写多 | sync.RWMutex + map |
| 键集合稳定 | sync.Map(内部优化) |
| 需要遍历同时修改 | sync.Map(Range 安全) |
| 简单场景 | sync.RWMutex + map |
9.7 Map 的内部实现
Go 的 map 底层是哈希表(hash table),采用链地址法处理冲突:
bucket 数组
┌─────────────────────────────────────┐
│ bucket 0 │ bucket 1 │ bucket 2 │ ...│
└─────────────────────────────────────┘
│
▼
┌──────────────┐
│ tophash[8] │ ← 每个 bucket 存 8 个键值对
│ keys[8] │
│ values[8] │
│ overflow ────────→ 溢出 bucket
└──────────────┘
// 观察 map 的内存占用
import (
"fmt"
"runtime"
)
func main() {
var m1 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m1)
data := make(map[int]int, 1_000_000)
for i := 0; i < 1_000_000; i++ {
data[i] = i
}
runtime.GC()
var m2 runtime.MemStats
runtime.ReadMemStats(&m2)
fmt.Printf("1M 键值对占用: %.2f MB\n", float64(m2.Alloc-m1.Alloc)/1024/1024)
}
9.8 Map 性能优化
import "testing"
// 预分配容量
func BenchmarkMapPrealloc(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]int, 10000)
for j := 0; j < 10000; j++ {
m[j] = j
}
}
}
func BenchmarkMapNoPrealloc(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]int)
for j := 0; j < 10000; j++ {
m[j] = j
}
}
}
💡 技巧:
- 预估大小时用
make(map[K]V, size)预分配 - 大量删除后考虑重建 map(map 不会缩容)
- 小数据量考虑用 slice 替代(遍历性能更好)
🏢 业务场景
- 缓存系统:map + Mutex 实现本地缓存
- 计数器/统计:词频统计、UV 计数
- 路由表:HTTP 路由注册和匹配
- 配置管理:解析 JSON/YAML 配置为 map
- 去重:map[T]bool 作为集合去重