导航菜单

映射(Map)

Map(映射)是 Go 语言中用于存储键值对(Key-Value)的内置数据结构。它类似于其他语言中的哈希表、字典(Dictionary)或 HashMap,提供了近乎 O(1) 时间复杂度的查找、插入和删除操作。

定义与初始化

使用 make 创建

make 是创建 map 最常用的方式,它分配并初始化一个哈希表数据结构:

// make(map[KeyType]ValueType)
m := make(map[string]int)
fmt.Println(m) // map[] —— 空的 map

// 预分配空间(提升性能,减少扩容次数)
m2 := make(map[string]int, 100) // 预分配约 100 个键值对的空间

使用字面量初始化

// 使用字面量直接初始化
ages := map[string]int{
    "Alice":   30,
    "Bob":     25,
    "Charlie": 35,
}
fmt.Println(ages) // map[Alice:30 Bob:25 Charlie:35]

// 空的 map(非 nil)
emptyMap := map[string]int{}
fmt.Println(emptyMap == nil) // false

nil map 与空 map

// nil map —— 声明但未初始化
var nilMap map[string]int
fmt.Println(nilMap == nil)  // true
fmt.Println(len(nilMap))    // 0 —— len 对 nil map 也安全

// 空 map —— 已初始化但无元素
emptyMap := make(map[string]int)
fmt.Println(emptyMap == nil) // false
fmt.Println(len(emptyMap))   // 0
var m map[string]int

// 读取安全
v := m["key"]       // 返回零值 0,不 panic
fmt.Println(len(m)) // 0,不 panic

// 写入 panic
// m["key"] = 1      // panic: assignment to entry in nil map

// 正确做法:先初始化
m = make(map[string]int)
m["key"] = 1        // ✅ 正常

基本操作

增加与修改

m := make(map[string]int)

// 添加键值对
m["Go"] = 2009
m["Python"] = 1991
m["Rust"] = 2010

// 修改已有的键(覆盖旧值)
m["Python"] = 2024

fmt.Println(m) // map[Go:2009 Python:2024 Rust:2010]

查找(判断 key 是否存在)

在 Go 中,通过 map 查找不存在的 key 时,不会像某些语言那样抛出异常,而是返回 value 类型的零值。因此需要使用 comma ok 惯用法来判断 key 是否真正存在:

m := map[string]int{
    "Alice":   30,
    "Bob":     25,
}

// 普通查找:如果 key 不存在,返回零值
age := m["Alice"]
fmt.Println(age) // 30

age = m["Unknown"]
fmt.Println(age) // 0 —— 无法区分是 "Unknown: 0" 还是 "Unknown 不存在"
comma ok 惯用法

Go 使用 value, ok := m[key] 的形式来判断 key 是否存在于 map 中。value 是 key 对应的值(不存在时为零值),ok 是布尔值,表示 key 是否存在。

m := map[string]int{
    "Alice":   30,
    "Bob":     0,    // 值恰好为零值
}

// 使用 comma ok 判断 key 是否存在
if age, ok := m["Alice"]; ok {
    fmt.Printf("Alice 的年龄是 %d\n", age) // Alice 的年龄是 30
}

if age, ok := m["Unknown"]; ok {
    fmt.Printf("Unknown 的年龄是 %d\n", age)
} else {
    fmt.Println("Unknown 不存在于 map 中") // Unknown 不存在于 map 中
}

// 区分 "key 存在且值为 0" 和 "key 不存在"
if age, ok := m["Bob"]; ok {
    fmt.Printf("Bob 的年龄是 %d(key 存在)\n", age) // Bob 的年龄是 0(key 存在)
}

删除

使用内置的 delete 函数删除 map 中的键值对:

m := map[string]int{
    "Alice": 30,
    "Bob":   25,
    "Carol": 28,
}

delete(m, "Bob")
fmt.Println(m) // map[Alice:30 Carol:28]

// 删除不存在的 key 不会 panic,是安全的操作
delete(m, "Unknown") // 无任何效果

长度

使用 len 函数获取 map 中键值对的数量:

m := map[string]int{"a": 1, "b": 2, "c": 3}
fmt.Println(len(m)) // 3

delete(m, "a")
fmt.Println(len(m)) // 2

遍历

使用 for range 遍历 map:

m := map[string]int{
    "Go":     2009,
    "Python": 1991,
    "Rust":   2010,
    "Java":   1995,
}

// 同时遍历 key 和 value
for key, value := range m {
    fmt.Printf("%s: %d\n", key, value)
}

// 只遍历 key
for key := range m {
    fmt.Println(key)
}

// 只遍历 value
for _, value := range m {
    fmt.Println(value)
}
// 按固定顺序遍历 map
m := map[string]int{
    "Go": 2009, "Python": 1991, "Rust": 2010, "Java": 1995,
}

// 收集所有 key 并排序
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)

// 按排序后的 key 遍历
for _, k := range keys {
    fmt.Printf("%s: %d\n", k, m[k])
}

map 的内部结构

map 底层实现

Go 的 map 基于哈希表实现,核心是一个 hmap 结构体,包含桶数组(buckets)、哈希种子、元素数量等信息。每个桶存储 8 个键值对,溢出时使用溢出桶链表。当元素数量达到负载因子阈值(6.5)时,触发扩容。

┌─────────────────────────────────┐
│            hmap                  │
│  ┌──────────┐  ┌──────────┐    │
│  │ count    │  │ flags    │    │
│  ├──────────┤  ├──────────┤    │
│  │ B (桶数) │  │ hash0    │    │
│  └──────────┘  └──────────┘    │
│  ┌──────────────────────┐      │
│  │ buckets (桶数组指针)  │───┐  │
│  └──────────────────────┘   │  │
└─────────────────────────────│──┘

┌─────────┐  ┌─────────┐  ┌─────────┐
│ bucket 0 │  │ bucket 1 │  │ bucket N │  ...
│ ┌─────┐ │  │         │  │         │
│ │k0,v0│ │  │         │  │         │
│ │k1,v1│ │  │         │  │         │
│ │ ...  │ │  │         │  │         │
│ │k7,v7│ │  │         │  │         │
│ └─────┘ │  │         │  │         │
│ overflow│  │         │  │         │
└─────────┘  └─────────┘  └─────────┘

注意事项

无序性

如前所述,map 不保证遍历顺序。Go 1.12+ 版本会在每次 range 时使用随机起始位置来遍历桶,确保顺序不可预测。

m := map[string]int{"a": 1, "b": 2, "c": 3}

// 两次遍历的顺序可能不同
for k, v := range m {
    fmt.Printf("%s=%d ", k, v)
}
fmt.Println()

非线程安全

m := make(map[string]int)

// ❌ 并发写入 —— 会导致 fatal error
// go func() {
//     m["a"] = 1
// }()
// go func() {
//     m["b"] = 2
// }()

解决并发安全问题

方案一:使用 sync.Mutex 加锁

import (
    "fmt"
    "sync"
)

type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func NewSafeMap() *SafeMap {
    return &SafeMap{
        m: make(map[string]int),
    }
}

func (sm *SafeMap) Set(key string, value int) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[key] = value
}

func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    v, ok := sm.m[key]
    return v, ok
}

func (sm *SafeMap) Delete(key string) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    delete(sm.m, key)
}

方案二:使用 sync.Map(推荐用于特定场景)

var m sync.Map

// 存储
m.Store("Go", 2009)
m.Store("Python", 1991)

// 读取
if v, ok := m.Load("Go"); ok {
    fmt.Println(v) // 2009
}

// 读取或写入(如果 key 不存在,则调用 fn 计算)
v, loaded := m.LoadOrStore("Rust", 2010)
fmt.Println(v, loaded) // 2010 false(首次写入)

v, loaded = m.LoadOrStore("Rust", 9999)
fmt.Println(v, loaded) // 2010 true(key 已存在,返回旧值)

// 删除
m.Delete("Python")

// 遍历
m.Range(func(key, value any) bool {
    fmt.Printf("%v: %v\n", key, value)
    return true // 返回 false 停止遍历
})
sync.Map 的适用场景

sync.Map 针对两种场景进行了优化:

  1. Key 稳定、读多写少:一旦写入后很少修改,频繁读取
  2. 多个 goroutine 读写不同的 key:即”分散”的并发访问

对于一般的并发 map 场景,sync.RWMutex + map 通常是更通用、性能更好的选择。

引用类型

map 是引用类型。将 map 赋值给新变量或作为函数参数传递时,不会拷贝底层数据,而是共享同一个哈希表:

m1 := map[string]int{"a": 1, "b": 2}
m2 := m1 // m2 和 m1 指向同一个底层数据

m2["c"] = 3
fmt.Println(m1) // map[a:1 b:2 c:3] —— m1 也被修改了
fmt.Println(m1 == m2) // 编译错误:map 只能与 nil 比较

不能对 map 元素取地址

m := map[string]Person{
    "Alice": {Name: "Alice", Age: 30},
}

// ❌ 编译错误:cannot take the address of m["Alice"]
// p := &m["Alice"]

// ✅ 正确做法:先取出值到局部变量
person := m["Alice"]
person.Age = 31
m["Alice"] = person

// ✅ 或者使用指针作为 map 的 value
m2 := map[string]*Person{
    "Alice": &Person{Name: "Alice", Age: 30},
}
m2["Alice"].Age = 31 // 通过指针修改

map 的 key 类型限制

map 的 key 必须是可比较的(comparable),即支持 == 运算符。以下类型不能作为 key:

  • 切片(slice)
  • 函数(func)
  • map 本身
// ✅ 合法的 key 类型
m1 := map[string]int{}          // string
m2 := map[int]bool{}            // int
m3 := map[float64]string{}      // float64
m4 := map[bool]int{}            // bool
m5 := map[[3]int]string{}       // 数组(元素可比较即可)

// ❌ 非法的 key 类型
// m6 := map[[]int]string{}     // 编译错误:slice 作为 key
// m7 := map[map[string]int]int{} // 编译错误:map 作为 key
// m8 := map[func()]int{}       // 编译错误:func 作为 key

// ✅ 结构体可以作为 key(只要所有字段都可比较)
type Point struct {
    X, Y int
}
m := map[Point]bool{
    {1, 2}: true,
    {3, 4}: false,
}

练习题

练习 1:单词频率统计

编写一个函数,接收一个字符串切片,返回一个 map[string]int,统计每个单词出现的次数。

参考答案
package main

import (
    "fmt"
    "strings"
)

func wordCount(words []string) map[string]int {
    result := make(map[string]int)
    for _, word := range words {
        word = strings.ToLower(word)
        result[word]++
    }
    return result
}

func main() {
    words := []string{"Go", "go", "Python", "Go", "Rust", "python", "Go"}
    counts := wordCount(words)
    for word, count := range counts {
        fmt.Printf("%s: %d\n", word, count)
    }
    // 输出(顺序不确定):
    // go: 3
    // python: 2
    // rust: 1
}

练习 2:map 合并

编写一个函数,将两个 map[string]int 合并为一个新的 map。如果两个 map 中存在相同的 key,新 map 中该 key 的值应为两个 map 中对应值的和。

参考答案
package main

import "fmt"

func mergeMaps(a, b map[string]int) map[string]int {
    result := make(map[string]int, len(a)+len(b))

    // 复制第一个 map
    for k, v := range a {
        result[k] = v
    }

    // 合并第二个 map(相同 key 的值相加)
    for k, v := range b {
        result[k] += v
    }

    return result
}

func main() {
    m1 := map[string]int{"a": 1, "b": 2, "c": 3}
    m2 := map[string]int{"b": 10, "c": 20, "d": 30}

    merged := mergeMaps(m1, m2)
    fmt.Println(merged) // map[a:1 b:12 c:23 d:30]
}

练习 3:安全的 map 操作

编写一个并发安全的 map 计数器,使用 sync.Mutex 保护。提供 Increment(key)Get(key)Count() 三个方法。

参考答案
package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu sync.Mutex
    m  map[string]int
}

func NewCounter() *Counter {
    return &Counter{m: make(map[string]int)}
}

func (c *Counter) Increment(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.m[key]++
}

func (c *Counter) Get(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.m[key]
}

func (c *Counter) Count() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return len(c.m)
}

func main() {
    counter := NewCounter()

    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            key := fmt.Sprintf("worker-%d", id%5)
            counter.Increment(key)
        }(i)
    }
    wg.Wait()

    for i := 0; i < 5; i++ {
        key := fmt.Sprintf("worker-%d", i)
        fmt.Printf("%s: %d\n", key, counter.Get(key))
    }
    fmt.Println("总 key 数:", counter.Count())
}

搜索