Skip to content

1.6 数据类型:字典 (Map)

在 Go 语言里,映射(Map)是一种无序的键值对(Key-Value)集合,能实现快速查找、存储与删除数据。

Map 类型的字典,由多个 key:value 键值对组合而成。它基于哈希表实现,要求每个键(key)必须唯一,可通过 == 和 != 进行判等操作,即键必须是可哈希的。

所谓可哈希,简单讲就是不可变对象能用唯一哈希值表示。因此,切片(Slice)、映射(Map)和函数(Function) 这几种内建类型因其不可比较性而不能作为 Map 的键,而其他多数内建类型,如字符串、整数等,都满足可哈希条件。

一、Map 的核心特性与定义

1. 核心特性

特性描述
无序性Map 中的元素没有固定的顺序,遍历时的顺序是随机的。
键值对Map 存储的是 Key 和 Value 组成的配对数据。
引用类型Map 是引用类型。赋值和传参时,传递的是 Map 结构的副本,但它们都指向同一个底层数据结构,修改会影响原始 Map。
必须初始化Map 的零值是 nilnil Map 不能直接赋值,必须使用 make() 函数或字面量进行初始化才能使用。
键的限制Map 的键(Key)必须是可比较的类型,例如:布尔型、整型、浮点型、字符串、数组、结构体(如果其所有字段都可比较),切片、函数和包含切片的结构体不能作为 Map 的键

2. Map 的定义

Map 的定义语法:map[KeyType]ValueType

go
// 定义一个键是 string 类型,值是 int 类型的 Map
var scoreMap map[string]int

3. Map 的初始化

由于 Map 是引用类型,必须初始化才能使用。

(1) 使用 make() 函数初始化

make() 函数用于分配内存。建议在初始化时指定一个合适的容量 (cap),以减少后续扩容的开销。

go
// 语法:make(map[KeyType]ValueType, [cap])

// 初始化一个容量为 8 的 Map
scoreMap := make(map[string]int, 8) 

// 往 Map 中添加元素
scoreMap["张三"] = 90
scoreMap["小明"] = 100

(2) 使用字面量初始化

可以在声明的同时直接初始化元素。

go
// 声明并填充元素
userInfo := map[string]string{
    "username": "alice",
    "password": "123",
}

// 声明一个空 Map
emptyMap := map[int]string{}

(3) 致命错误:不声明内存直接使用 (Panic)

Go 语言中的 var 声明会给 Map 变量赋予其零值,即 nil。试图向一个 nil Map 中添加元素会导致程序运行时崩溃 (panic)。

go
package main

import "fmt"

func main() {
    // 错误示例:Map 零值为 nil
    var scoreMap map[string]int 
    // ❌ 运行时错误 (panic): assignment to entry in nil map
    // 此时 scoreMap 还没有分配内存空间
    scoreMap["张三"] = 90 
    fmt.Println(scoreMap) // 这行代码通常不会执行到
	// 使用 make() 函数初始化
	// 这是最标准和推荐的初始化方式,可以预先指定容量。

	// 正确初始化:使用 make 分配内存
	scoreMap := make(map[string]int) 
	// 或者指定容量
	// scoreMap := make(map[string]int, 10) 
	scoreMap["张三"] = 90 // 成功赋值
}

二、Map 的基本操作

1. 存取键值对

通过键(Key)可以直接存取对应的值(Value)。

go
myMap := make(map[string]int)

// 存值/新增:
myMap["apple"] = 5
myMap["banana"] = 10

// 取值:
appleCount := myMap["apple"] // 5
nonExistent := myMap["grape"] // 0 (取不存在的键,返回 ValueType 的零值)

2. 判断键是否存在

Go 语言提供了一个特殊的写法来判断 Map 中某个键是否存在,这是安全地获取值并判断键是否存在的最佳方式。

go
value, ok := myMap[key]
  • value:如果键存在,则为对应的值;如果键不存在,则为值类型的零值
  • ok:一个布尔值,如果键存在则为 true,否则为 false
go
scoreMap := map[string]int{"张三": 90, "小明": 100}

// 查找 "张三"
v1, ok1 := scoreMap["张三"]
if ok1 {
    fmt.Println("张三的分数是:", v1) // 90
}

// 查找 "李四"
v2, ok2 := scoreMap["李四"]
if !ok2 {
    fmt.Println("查无此人,返回的零值是:", v2) // 0
}

3. 删除键值对

使用内置函数 delete() 从 Map 中删除一组键值对。

go
delete(map, key)
  • 如果 key 不存在,delete() 函数不会报错,什么也不会发生。
go
scoreMap := map[string]int{"张三": 90, "小明": 100, "王五": 60}

delete(scoreMap, "小明") 
fmt.Println(scoreMap) // map[张三:90 王五:60]

delete(scoreMap, "不存在的键") // 不会报错

4. 遍历 Map

使用 for range 循环遍历 Map。

go
scoreMap := map[string]int{"张三": 90, "小明": 100, "王五": 60}

// 遍历键和值
for k, v := range scoreMap {
    fmt.Printf("键: %s, 值: %d\n", k, v)
}

// 只遍历键
for k := range scoreMap {
    fmt.Println("键:", k)
}

// 只遍历值
for _, v := range scoreMap {
    fmt.Println("值:", v)
}

注意: Map 是无序的,每次遍历的元素顺序都可能不同。

5. 按照指定顺序遍历 Map

如果需要按特定的顺序(如 Key 的字典序)遍历 Map,则需要借助切片(Slice)来实现:

  1. 将 Map 的所有 Key 取出,存入一个切片中。
  2. 对该 Key 切片进行排序(例如使用 sort.Strings)。
  3. 按照排序后的 Key 切片依次从 Map 中取值并遍历。
go
package main

import (
    "fmt"
    "sort"
)

func main() {
    scoreMap := map[string]int{
        "stu03": 90,
        "stu01": 100,
        "stu02": 85,
    }

    // 1. 取出所有 Key
    var keys []string
    for k := range scoreMap {
        keys = append(keys, k)
    }

    // 2. 对 Key 进行排序
    sort.Strings(keys) 

    // 3. 按排序后的 Key 遍历 Map
    for _, k := range keys {
        fmt.Printf("%s 的分数是 %d\n", k, scoreMap[k])
    }
    // 输出顺序:stu01, stu02, stu03
}

三、Map 的组合应用

1. 元素为 Map 类型的切片

切片(Slice)可以存储任意类型的元素,包括 Map。这常用于表示一个记录列表

go
package main

import "fmt"

func main() {
    // 声明并初始化一个包含 3 个 Map 的切片
    var mapSlice = make([]map[string]string, 3) 
    
    // 必须对切片中的每个 Map 元素进行初始化才能使用
    mapSlice[0] = make(map[string]string)
    mapSlice[0]["name"] = "Alice"
    mapSlice[0]["city"] = "Beijing"

    mapSlice[1] = map[string]string{"name": "Bob", "city": "Shanghai"}

    fmt.Println(mapSlice) 
    // [[name:Alice city:Beijing] [name:Bob city:Shanghai] map[]]
}

2. 值为切片类型的 Map

Map 的值(Value)可以是切片类型,这常用于实现分组多值映射

go
package main

import "fmt"

func main() {
    // 声明一个键为 string,值为 []string 的 Map
    var sliceMap = make(map[string][]string) 

    // 为 "Fruit" 键添加值
    key := "Fruit"
    
    // 检查键是否存在,如果不存在则初始化切片
    value, ok := sliceMap[key]
    if !ok {
        value = make([]string, 0)
    }
    
    // 向切片中追加数据
    value = append(value, "Apple", "Banana", "Grape") 
    
    // 将更新后的切片重新赋值给 Map
    sliceMap[key] = value 

    fmt.Println(sliceMap) // map[Fruit:[Apple Banana Grape]]
}

四、Map 的常见问题与注意事项

1. Map 的并发安全

Go 语言内置的 Map 在并发场景下是不安全的。

当多个 Goroutine 同时对一个 Map 进行读写操作时(即使是多个读操作也可能在某些 Go 版本中导致问题,但主要是读写冲突),会引发竞态条件(Race Condition),导致程序崩溃(panic)。

解决方案

如果需要在多个 Goroutine 中安全地使用 Map,必须采取同步措施。

a. 使用互斥锁(sync.Mutex

这是最常见和通用的做法,通过在操作 Map 之前加锁、操作完成后解锁来保证同一时间只有一个 Goroutine 访问 Map。

go
package main

import (
    "fmt"
    "sync"
)

// 创建一个互斥锁
var safeMapLock sync.Mutex

// 创建一个全局的 map
var safeMap = make(map[string]int)

// Set 函数用于向 map 中安全地设置键值对
func Set(key string, value int) {
    safeMapLock.Lock()
    defer safeMapLock.Unlock()
    safeMap[key] = value
}

// Get 函数用于从 map 中安全地获取值
func Get(key string) (int, bool) {
    safeMapLock.Lock()
    defer safeMapLock.Unlock()
    v, ok := safeMap[key]
    return v, ok
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(index int) {
            key := fmt.Sprintf("key%d", index)
            Set(key, index)
            wg.Done()
        }(i)
    }

    wg.Wait()

    fmt.Println("并发写入完成,Map 大小:", len(safeMap))
}

b. 使用 sync.Map

Go 语言在 sync 包中提供了开箱即用的并发安全 Mapsync.Map 针对高并发的常见场景进行了优化,性能优于直接使用 sync.Mutex 保护的原生 Map。

go
// sync.Map 的 API 是 Store/Load/Delete/Range,与原生 Map 不同

var safeMap sync.Map 

// 存储
safeMap.Store("key1", 100) 

// 读取
value, ok := safeMap.Load("key1") 
if ok {
    fmt.Println("读取的值:", value) 
}

// 删除
safeMap.Delete("key1")

2. Map 容量

虽然可以在创建 map 时指定容量,但 map 会自动增长以容纳更多的键值对。指定初始容量只是一个性能优化手段。

go
// 创建指定容量的map
scores := make(map[string]int, 100)

3. 不能对 map 元素取地址

go
// 错误:不能对map元素取地址
score := &scores["张三"] // 编译错误

4. Map 的零值

go
// Map 的零值是 nil
var m map[string]int
fmt.Println(m == nil) // true
// 不能向 nil map 添加元素
m["张三"] = 100 // 运行时错误:assignment to entry in nil map

五、Map 的实际应用场景

1. 简单计数器(函数映射)

go
// 统计字符串中各字符出现的次数
func countCharacters(s string) map[rune]int {
    counter := make(map[rune]int)
    for _, char := range s {
        counter[char]++
    }
    return counter
}

str := "hello, 世界"
counts := countCharacters(str)
for char, count := range counts {
    fmt.Printf("字符 '%c' 出现 %d\n", char, count)
}

2. 缓存

go
// 简单的缓存实现
cache := make(map[string]string)

// 设置缓存
cache["key1"] = "value1"

// 获取缓存
if value, ok := cache["key1"]; ok {
    fmt.Println("缓存命中:", value)
} else {
    fmt.Println("缓存未命中")
}

3. 分组/过滤

go
package main

import "fmt"

type Student struct {
	Name  string
	Age   int
	Grade string
}

func main() {
	// 示例数据
	students := []Student{
		{"Alice", 20, "A"},
		{"Bob", 21, "B"},
		{"Charlie", 20, "A"},
		{"David", 22, "B"},
	}

	// 按年龄分组
	ageGroups := make(map[int][]Student)
	for _, student := range students {
		ageGroups[student.Age] = append(ageGroups[student.Age], student)
	}

	// 按成绩过滤
	gradeFilter := make(map[string][]string)
	for _, student := range students {
		gradeFilter[student.Grade] = append(gradeFilter[student.Grade], student.Name)
	}

	// 输出分组结果
	fmt.Println("按年龄分组:")
	for age, group := range ageGroups {
		fmt.Printf("年龄 %d: %v\n", age, group)
	}

	fmt.Println("\n按成绩分组:")
	for grade, names := range gradeFilter {
		fmt.Printf("成绩 %s: %v\n", grade, names)
	}
}