Map
Map
想象你在查字典:你输入一个单词(key),就能立刻找到它的释义(value)。不需要从第一页翻到最后一页,而是直接定位到目标页。Go 语言中的 map 就是这样一个"字典"——它是一种键值对(key-value)数据结构,能让你通过 key 快速检索 value,时间复杂度为 O(1)。
1. 核心概念
| 概念 | 说明 |
|---|---|
| 定义 | map[KeyType]ValueType,KeyType 必须是可比较类型(不能是 slice、map、func) |
| 创建方式 | make(map[K]V) 或 map[K]V{} 字面量 |
| 增/改 | m[key] = value(key 不存在则新增,存在则覆盖) |
| 查 | v := m[key] |
| 删 | delete(m, key) |
| comma ok 模式 | v, ok := m[key],ok 为 false 表示 key 不存在 |
| 遍历 | for k, v := range m(顺序随机,不可依赖) |
| 长度 | len(m) |
⚠️ 注意:map 是引用类型,赋值和传参传递的是引用,不会复制数据。
2. 基本语法/用法
创建 map
GO
// 方式一:使用 make 创建
m1 := make(map[string]int)
// 方式二:字面量创建并初始化
m2 := map[string]int{
"apple": 5,
"banana": 3,
}
// 方式三:声明后赋值
var m3 map[string]int // 此时 m3 是 nil,不能直接赋值
m3 = make(map[string]int) // 先初始化
m3["cherry"] = 7 // 再赋值
💡 提示:
var m map[K]V 声明的 map 是 nil,不能写入(会 panic),但可以读取(返回零值)和 len()(返回 0)。
增删改查
GO
m := map[string]int{"a": 1, "b": 2}
// 增
m["c"] = 3
// 改
m["a"] = 10
// 查
v := m["a"] // v = 10
// 删
delete(m, "b")
💡 提示:删除一个不存在的 key 不会报错,也不会发生任何事情。
Comma Ok 模式
GO
m := map[string]int{"x": 42}
v, ok := m["x"] // v = 42, ok = true
v, ok = m["y"] // v = 0, ok = false(返回 int 的零值)
if _, exists := m["z"]; !exists {
fmt.Println("key 'z' 不存在")
}
💡 提示:如果不关心 value,可以用
_ 忽略:_, ok := m[key]。
3. 示例代码
示例 1:基础用法(难度⭐)
创建一个学生成绩表,进行增删改查操作。
GO
package main
import "fmt"
func main() {
// 创建学生成绩 map
scores := map[string]int{
"Alice": 90,
"Bob": 85,
}
// 新增一个学生
scores["Charlie"] = 92
// 修改 Bob 的成绩
scores["Bob"] = 88
// 查询 Alice 的成绩
fmt.Println("Alice 的成绩:", scores["Alice"])
// 删除 Charlie
delete(scores, "Charlie")
// 遍历所有学生成绩
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
fmt.Println("学生人数:", len(scores))
}
输出示例(遍历顺序可能不同):
TEXT
Alice 的成绩: 90
Bob: 88
Alice: 90
学生人数: 2
示例 2:进阶用法(难度⭐⭐)
使用 comma ok 模式安全查询,遍历 map,以及嵌套 map 的使用。
GO
package main
import "fmt"
func main() {
// ========== comma ok 模式 ==========
fruits := map[string]int{
"apple": 5,
"banana": 3,
}
// 安全查询
if count, ok := fruits["apple"]; ok {
fmt.Printf("apple 有 %d 个\n", count)
}
if count, ok := fruits["grape"]; !ok {
fmt.Println("grape 不存在,将添加到列表中")
fruits["grape"] = 10
}
// ========== range 遍历 ==========
fmt.Println("\n所有水果:")
for fruit, count := range fruits {
fmt.Printf(" %s: %d\n", fruit, count)
}
// ========== 嵌套 map(map of maps)==========
// 班级 -> 学生 -> 成绩
classScores := map[string]map[string]int{
"一班": {
"Alice": 90,
"Bob": 85,
},
"二班": {
"Charlie": 92,
"Diana": 88,
},
}
// 查询嵌套 map
if class, ok := classScores["一班"]; ok {
if score, ok := class["Alice"]; ok {
fmt.Printf("\n一班 Alice 的成绩: %d\n", score)
}
}
// 遍历嵌套 map
fmt.Println("\n所有班级成绩:")
for class, students := range classScores {
fmt.Printf(" %s:\n", class)
for name, score := range students {
fmt.Printf(" %s: %d\n", name, score)
}
}
}
输出:
TEXT
apple 有 5 个
grape 不存在,将添加到列表中
所有水果:
apple: 5
banana: 3
grape: 10
一班 Alice 的成绩: 90
所有班级成绩:
一班:
Alice: 90
Bob: 85
二班:
Charlie: 92
Diana: 88
示例 3:综合应用(难度⭐⭐⭐)
实现一个词频统计器,并基于 map 进行数据处理(排序输出、找出高频词)。
GO
package main
import (
"fmt"
"sort"
"strings"
)
// WordCounter 统计文本中每个单词出现的次数
func WordCounter(text string) map[string]int {
// 转为小写并按空格分割
words := strings.Fields(strings.ToLower(text))
// 创建词频 map
freq := make(map[string]int)
for _, word := range words {
// 去除标点符号(简单处理)
word = strings.Trim(word, ".,!?;:\"'")
if word != "" {
freq[word]++
}
}
return freq
}
// TopN 返回频率最高的 N 个单词(按频率降序)
func TopN(freq map[string]int, n int) []string {
// 将 map 转换为可排序的切片
type wordFreq struct {
word string
count int
}
// 构造切片
pairs := make([]wordFreq, 0, len(freq))
for w, c := range freq {
pairs = append(pairs, wordFreq{w, c})
}
// 按频率降序排序
sort.Slice(pairs, func(i, j int) bool {
if pairs[i].count == pairs[j].count {
return pairs[i].word < pairs[j].word // 频率相同按字母排序
}
return pairs[i].count > pairs[j].count
})
// 取前 N 个
if n > len(pairs) {
n = len(pairs)
}
result := make([]string, n)
for i := 0; i < n; i++ {
result[i] = fmt.Sprintf("%s(%d)", pairs[i].word, pairs[i].count)
}
return result
}
// MergeFreq 合并两个词频 map
func MergeFreq(a, b map[string]int) map[string]int {
result := make(map[string]int)
// 复制 a 的数据
for k, v := range a {
result[k] = v
}
// 累加 b 的数据
for k, v := range b {
result[k] += v
}
return result
}
func main() {
text1 := "Go is great. Go is fast. Go is easy to learn."
text2 := "I love Go. Go makes programming fun and easy."
// 统计词频
freq1 := WordCounter(text1)
freq2 := WordCounter(text2)
fmt.Println("文本1 词频:")
for word, count := range freq1 {
fmt.Printf(" %s: %d\n", word, count)
}
fmt.Println("\n文本2 词频:")
for word, count := range freq2 {
fmt.Printf(" %s: %d\n", word, count)
}
// 合并词频
merged := MergeFreq(freq1, freq2)
// 找出频率最高的 5 个单词
fmt.Println("\n合并后 Top 5 高频词:")
top5 := TopN(merged, 5)
for i, item := range top5 {
fmt.Printf(" %d. %s\n", i+1, item)
}
// 统计总词数
totalWords := 0
for _, count := range merged {
totalWords += count
}
fmt.Printf("\n总词数: %d,不同单词数: %d\n", totalWords, len(merged))
}
输出示例:
TEXT
文本1 词频:
go: 3
is: 3
great: 1
fast: 1
easy: 1
to: 1
learn: 1
文本2 词频:
i: 1
love: 1
go: 2
makes: 1
programming: 1
fun: 1
and: 1
easy: 1
合并后 Top 5 高频词:
1. go(5)
2. is(3)
3. easy(2)
4. and(1)
5. fast(1)
总词数: 18,不同单词数: 12
3. 常见应用场景
场景一:缓存/快速查找
用 map 实现一个简单的内存缓存,避免重复计算。
GO
package main
import "fmt"
// 斐波那契数列(带缓存)
var cache = map[int]int{0: 0, 1: 1}
func fib(n int) int {
// 先查缓存
if val, ok := cache[n]; ok {
return val
}
// 缓存未命中,计算并存入缓存
result := fib(n-1) + fib(n-2)
cache[n] = result
return result
}
func main() {
for i := 0; i <= 10; i++ {
fmt.Printf("fib(%d) = %d\n", i, fib(i))
}
fmt.Println("\n缓存内容:", cache)
}
场景二:分组统计
用 map 对数据进行分组统计。
GO
package main
import "fmt"
func main() {
// 学生列表:名字 -> 班级
students := map[string]string{
"Alice": "一班",
"Bob": "二班",
"Charlie": "一班",
"Diana": "二班",
"Eve": "一班",
}
// 按班级分组
groups := make(map[string][]string)
for name, class := range students {
groups[class] = append(groups[class], name)
}
// 输出分组结果
for class, members := range groups {
fmt.Printf("%s (%d人): %v\n", class, len(members), members)
}
}
输出:
TEXT
一班 (3人): [Alice Charlie Eve]
二班 (2人): [Bob Diana]
❓ 常见问题
Q1:为什么遍历 map 的顺序每次都不一样?
Go 语言故意让 map 的遍历顺序随机化。这是为了防止开发者依赖 map 的遍历顺序,因为在并发场景下 map 的内部结构可能发生变化。如果需要有序遍历,应该先将 key 收集到切片中,排序后再遍历:
GO
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}
Q2:map 是线程安全的吗?
不是。 Go 的 map 不是并发安全的,多个 goroutine 同时读写同一个 map 会导致 panic。解决方案:
- 使用
sync.Mutex或sync.RWMutex保护 map - 使用 Go 1.9+ 提供的
sync.Map(适用于读多写少的场景)
GO
// 使用 sync.Map
var m sync.Map
m.Store("key", "value")
v, ok := m.Load("key")
Q3:map 的 key 可以用哪些类型?
map 的 key 必须是可比较类型,包括:
- 基本类型:
int、float64、string、bool - 指针
- 数组(元素可比较)
- 结构体(所有字段可比较)
不能作为 key 的类型:slice、map、func。
Q4:如何判断一个 map 是否包含某个 key?
使用 comma ok 模式:
GO
if _, ok := m[key]; ok {
// key 存在
} else {
// key 不存在
}
不要通过判断 value 是否为零值来判断 key 是否存在,因为零值也可能是合法的 value。
📖 小节
- map 是 Go 语言的内置键值对数据结构,通过 key 快速查找 value
- 使用
make或字面量map[K]V{}创建,var声明的 nil map 只读不写 - 增改:
m[key] = value,查:v := m[key],删:delete(m, key) - 使用 comma ok 模式(
v, ok := m[key])安全判断 key 是否存在 - 使用
for k, v := range m遍历,顺序随机,不可依赖 - map 是引用类型,赋值和传参不会复制数据
- map 非并发安全,多 goroutine 读写需加锁或使用
sync.Map - map vs slice:频繁查找用 map,有序存储用 slice;两者常配合使用
📝 作业
练习 1(⭐)
创建一个 map 存储 5 个编程语言及其发明年份,然后:
- 添加 2 门新语言
- 修改一门语言的年份
- 删除一门语言
- 使用 comma ok 模式查询一门语言是否存在
- 遍历并打印所有内容
练习 2(⭐⭐)
实现一个简单的通讯录程序:
- 定义
map[string][]string,key 为联系人姓名,value 为电话号码列表 - 实现添加联系人、添加电话号码、查找联系人、删除联系人功能
- 实现按首字母分组显示所有联系人
GO
// 预期输出示例:
// A: Alice - [13800001111, 13900002222]
// B: Bob - [13700003333]
练习 3(⭐⭐⭐)
实现一个学生成绩管理系统:
- 使用嵌套
map[string]map[string]float64(班级 -> 学生 -> 成绩) - 实现功能:添加成绩、查询某学生所有科目成绩、计算某班级平均分
- 实现功能:找出所有班级中每个科目的最高分学生
- 将结果按格式化表格输出
GO
// 预期输出示例:
// ========== 班级平均分 ==========
// 一班: 87.5
// 二班: 91.2
//
// ========== 各科最高分 ==========
// 数学: Alice (98.0)
// 英语: Bob (95.0)



