强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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, stringslice
bool, rune, bytemap
pointerfunc
arraystruct(含不可比较字段)
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", "alice@example.com")
    fmt.Println(actual2, loaded2) // alice@example.com, 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 替代(遍历性能更好)

🏢 业务场景

  1. 缓存系统:map + Mutex 实现本地缓存
  2. 计数器/统计:词频统计、UV 计数
  3. 路由表:HTTP 路由注册和匹配
  4. 配置管理:解析 JSON/YAML 配置为 map
  5. 去重:map[T]bool 作为集合去重

📖 扩展阅读