第 6 课:Map 哈希表
所属模块: 基础入门 难度: beginner 标签: map, key-value, delete 上一课: 数组与切片 下一课: 字符串与 rune
1. 学习目标
完成本课学习后,你将能够:
- 理解 Map(映射)数据结构的概念及其在 Go 中的作用。
- 掌握 Map 的声明、初始化、添加、访问、修改和删除元素的操作。
- 学会使用
for range遍历 Map 中的键值对。 - 了解 Map 的零值、
nilMap 的概念及其使用注意事项。 - 特别提醒:认识到 Map 在 Go 中不是并发安全的,为后续并发编程课程做铺垫。
2. 核心概念
什么是 Map?
想象一下你的手机通讯录或一本字典:你通过一个唯一的标识(名字或单词)来查找对应的信息(电话号码或释义)。Map(映射) 在 Go 语言中就是这样一个存储 “键-值对” 的数据结构。你提供一个 Key(键),Map 会告诉你对应的 Value(值) 是什么。
- 键(Key):必须是可比较的类型(如
string,int,bool, 指针等),不能是slice、map或func。 - 值(Value):可以是任意类型,包括另一个 Map 或 Slice。
声明与初始化
- 声明一个 nil Map:
var m map[string]int。此时m是nil,对它进行添加操作会引发 panic(恐慌),必须先初始化。 - 使用字面量初始化:
m := map[string]int{“apple”: 5, “banana”: 3}。 - 使用
make函数初始化:m := make(map[string]int)。这是创建空 Map 的推荐方式。
核心操作
- 添加/修改:
m[key] = value。如果键已存在,则修改其值;如果键不存在,则添加一对新键值。 - 访问:
value, ok := m[key]。ok是一个布尔值,表示键是否存在。强烈建议总是检查ok,以避免获得零值带来的误解。 - 删除:
delete(m, key)。如果键不存在,此操作无副作用,不会报错。 - 获取长度:
len(m)返回 Map 中键值对的数量。
3. 代码示例
以下是一个完整的、可运行的示例,演示了 Map 的基本操作:
package main
import "fmt"
func main() {
// 1. 使用 make 初始化一个空 Map
studentScores := make(map[string]int)
fmt.Println("初始 Map:", studentScores, "长度:", len(studentScores))
// 2. 添加键值对
studentScores["Alice"] = 95
studentScores["Bob"] = 88
studentScores["Charlie"] = 72
fmt.Println("添加数据后:", studentScores, "长度:", len(studentScores))
// 3. 访问元素(推荐方式:检查键是否存在)
score, ok := studentScores["Alice"]
if ok {
fmt.Printf("Alice 的分数是: %d\n", score)
} else {
fmt.Println("未找到 Alice 的分数")
}
// 访问一个不存在的键
_, ok = studentScores["Dave"]
if !ok {
fmt.Println("Dave 不在我们的成绩单中")
}
// 4. 修改元素
studentScores["Bob"] = 90 // 将 Bob 的分数从 88 改为 90
fmt.Println("修改 Bob 的分数后:", studentScores)
// 5. 删除元素
delete(studentScores, "Charlie")
fmt.Println("删除 Charlie 后:", studentScores, "长度:", len(studentScores))
// 6. 使用字面量初始化并遍历 Map
capitals := map[string]string{
"China": "Beijing",
"USA": "Washington D.C.",
"Japan": "Tokyo",
"France": "Paris",
}
fmt.Println("\n世界首都:")
for country, city := range capitals {
fmt.Printf("%s 的首都是 %s\n", country, city)
}
}
输出示例:
初始 Map: map[] 长度: 0
添加数据后: map[Alice:95 Bob:88 Charlie:72] 长度: 3
Alice 的分数是: 95
Dave 不在我们的成绩单中
修改 Bob 的分数后: map[Alice:95 Bob:90 Charlie:72]
删除 Charlie 后: map[Alice:95 Bob:90] 长度: 2
世界首都:
China 的首都是 Beijing
USA 的首都是 Washington D.C.
Japan 的首都是 Tokyo
France 的首都是 Paris
4. 实践练习
练习题 1:基础操作
创建一个 map[string][]string,键是编程语言的名字,值是存储该语言代表框架的字符串切片。完成以下操作:
- 初始化 Map 并添加 “Go”: [“Gin”, “Beego”], “Python”: [“Django”, “Flask”]。
- 访问并打印 “Python” 对应的框架列表。
- 为 “JavaScript” 添加一个键,其值为 [“React”, “Vue”]。
- 删除 “Go” 这个键。
预期输出:
Python 框架: [Django Flask]
Map 更新后: map[Go:[Gin Beego] JavaScript:[React Vue] Python:[Django Flask]]
删除后: map[JavaScript:[React Vue] Python:[Django Flask]]
练习题 2:字符频率统计
编写一个函数 countChars(s string) map[string]int,它接收一个字符串,返回一个 Map,其中键是字符串中出现的每个字符(转为 string 类型),值是该字符出现的次数。
- 提示:可以使用
for _, c := range s来遍历字符串中的字符(rune)。
示例调用与预期输出:
fmt.Println(countChars("hello"))
// 输出: map[h:1 e:1 l:2 o:1]
练习题 3:查找最大值(稍有挑战)
编写一个函数,接收一个 map[string]int 作为参数,找出并返回 Map 中值最大的那个键。
- 如果 Map 为空,返回空字符串
""。
示例调用与预期输出:
scores := map[string]int{"Math": 90, "English": 85, "History": 95}
topSubject := findTopKey(scores)
fmt.Println(topSubject) // 输出: History
5. 常见错误
-
对
nilMap 进行写操作:var m map[string]int // m 是 nil m["key"] = 1 // 运行时错误:panic: assignment to entry in nil map正确做法:在使用前用
make或字面量初始化。 -
忽略键值存在性检查:
m := make(map[string]int) value := m["missing_key"] // value 将是 0,你无法区分是“键不存在”还是“键的值本就是0”。正确做法:总是使用
value, ok := m[key]的形式。 -
并发读写 Map(非常重要!): Go 的内建 Map 不是并发安全的。如果多个 Goroutine 同时读写一个 Map,程序会发生数据竞争,导致不可预测的行为甚至崩溃。
// 危险!以下代码是错误示例! m := make(map[int]int) go func() { m[1] = 1 }() // Goroutine 1 写 go func() { fmt.Println(m[1]) }() // Goroutine 2 读解决方法:
- 使用
sync.Mutex或sync.RWMutex对 Map 的访问进行加锁。 - 使用 Go 1.9 引入的并发安全 Map:
sync.Map。(这通常在更高级的并发课程中介绍)
- 使用
6. 小结
- Map 是存储键-值对的集合,键必须可比较。
- 使用
make或字面量初始化,切勿对nilMap 进行写操作。 - 核心操作:
m[k]=v(添加/修改),v, ok:=m[k](访问,检查存在性),delete(m, k)(删除)。 - 使用
for k, v := range m来遍历 Map。 - 最重要的警告:Go 的内建 Map 不是并发安全的!在多 Goroutine 环境下操作同一个 Map,必须自行实现同步机制(如加锁)或使用
sync.Map。这是 Go 并发编程中一个基础但关键的概念。
练习编辑器
go
Loading...