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 的零值是 nil,nil Map 不能直接赋值,必须使用 make() 函数或字面量进行初始化才能使用。 |
| 键的限制 | Map 的键(Key)必须是可比较的类型,例如:布尔型、整型、浮点型、字符串、数组、结构体(如果其所有字段都可比较),切片、函数和包含切片的结构体不能作为 Map 的键。 |
2. Map 的定义
Map 的定义语法:map[KeyType]ValueType
// 定义一个键是 string 类型,值是 int 类型的 Map
var scoreMap map[string]int3. Map 的初始化
由于 Map 是引用类型,必须初始化才能使用。
(1) 使用 make() 函数初始化
make() 函数用于分配内存。建议在初始化时指定一个合适的容量 (cap),以减少后续扩容的开销。
// 语法:make(map[KeyType]ValueType, [cap])
// 初始化一个容量为 8 的 Map
scoreMap := make(map[string]int, 8)
// 往 Map 中添加元素
scoreMap["张三"] = 90
scoreMap["小明"] = 100(2) 使用字面量初始化
可以在声明的同时直接初始化元素。
// 声明并填充元素
userInfo := map[string]string{
"username": "alice",
"password": "123",
}
// 声明一个空 Map
emptyMap := map[int]string{}(3) 致命错误:不声明内存直接使用 (Panic)
Go 语言中的 var 声明会给 Map 变量赋予其零值,即 nil。试图向一个 nil Map 中添加元素会导致程序运行时崩溃 (panic)。
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)。
myMap := make(map[string]int)
// 存值/新增:
myMap["apple"] = 5
myMap["banana"] = 10
// 取值:
appleCount := myMap["apple"] // 5
nonExistent := myMap["grape"] // 0 (取不存在的键,返回 ValueType 的零值)2. 判断键是否存在
Go 语言提供了一个特殊的写法来判断 Map 中某个键是否存在,这是安全地获取值并判断键是否存在的最佳方式。
value, ok := myMap[key]value:如果键存在,则为对应的值;如果键不存在,则为值类型的零值。ok:一个布尔值,如果键存在则为true,否则为false。
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 中删除一组键值对。
delete(map, key)- 如果
key不存在,delete()函数不会报错,什么也不会发生。
scoreMap := map[string]int{"张三": 90, "小明": 100, "王五": 60}
delete(scoreMap, "小明")
fmt.Println(scoreMap) // map[张三:90 王五:60]
delete(scoreMap, "不存在的键") // 不会报错4. 遍历 Map
使用 for range 循环遍历 Map。
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)来实现:
- 将 Map 的所有 Key 取出,存入一个切片中。
- 对该 Key 切片进行排序(例如使用
sort.Strings)。 - 按照排序后的 Key 切片依次从 Map 中取值并遍历。
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。这常用于表示一个记录列表。
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)可以是切片类型,这常用于实现分组或多值映射。
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。
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。
// 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 会自动增长以容纳更多的键值对。指定初始容量只是一个性能优化手段。
// 创建指定容量的map
scores := make(map[string]int, 100)3. 不能对 map 元素取地址
// 错误:不能对map元素取地址
score := &scores["张三"] // 编译错误4. Map 的零值
// Map 的零值是 nil
var m map[string]int
fmt.Println(m == nil) // true
// 不能向 nil map 添加元素
m["张三"] = 100 // 运行时错误:assignment to entry in nil map五、Map 的实际应用场景
1. 简单计数器(函数映射)
// 统计字符串中各字符出现的次数
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. 缓存
// 简单的缓存实现
cache := make(map[string]string)
// 设置缓存
cache["key1"] = "value1"
// 获取缓存
if value, ok := cache["key1"]; ok {
fmt.Println("缓存命中:", value)
} else {
fmt.Println("缓存未命中")
}3. 分组/过滤
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)
}
}