Skip to content

1.7 数据类型:字典 (Map) ​

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

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

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

一、Map 的核心特性与定义 ​

1. 核心特性 ​

特性描述
无序性Map 中的元素没有固定的顺序,遍历时的顺序是随机的。
键值对Map 存储的是 Key 和 Value 组成的配对数据。
引用类型Map 是引用类型。赋值和传参时,传递的是 Map 结构的副本,但它们都指向同一个底层数据结构,修改会影响原始 Map。
必须初始化Map 的零值是 nil,nil 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 包中提供了开箱即用的并发安全 Map,sync.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)
	}
}