7.1 Go Map 基础
1. Map 基础 --- 创建、访问、删除
Map 是键值对的无序集合,类似其他语言的字典/哈希表:
go
package main
import "fmt"
func main() {
// make 创建空 map
ages := make(map[string]int)
// map 字面量创建并初始化
colors := map[string]string{
"red": "#FF0000",
"green": "#00FF00",
"blue": "#0000FF",
"white": "#FFFFFF",
"black": "#000000",
}
// 添加键值对
ages["Alice"] = 25
ages["Bob"] = 30
ages["Charlie"] = 35
// 访问值
aliceAge := ages["Alice"] // 25
// comma-ok 模式:检查键是否存在
bobAge, exists := ages["Bob"]
if exists {
fmt.Printf("Bob's age: %d\n", bobAge)
}
// 访问不存在的键:返回零值(不会报错!)
unknown := ages["Unknown"] // 返回 0(int 的零值)
fmt.Printf("Unknown person's age (zero value): %d\n", unknown)
// 删除键
delete(ages, "Alice") // 删除后 ages 只有 Bob 和 Charlie
// 获取长度
length := len(ages) // 2
// nil map:var 声明未初始化
var nilMap map[string]int // nilMap == nil
// nilMap["test"] = 1 // 会 panic!nil map 不能写入
if nilMap == nil {
fmt.Println("nilMap is nil - cannot assign to it")
}
fmt.Println("Ages map:", ages)
fmt.Println("Colors map:", colors)
fmt.Printf("Alice's age: %d\n", aliceAge)
fmt.Printf("Map length: %d\n", length)
}
执行结果:
Bob's age: 30
Unknown person's age (zero value): 0
nilMap is nil - cannot assign to it
Ages map: map[Bob:30 Charlie:35]
Colors map: map[black:#000000 blue:#0000FF green:#00FF00 red:#FF0000 white:#FFFFFF]
Alice's age: 25
Map length: 2
要点:
make(map[string]int)--- 创建空 map,可以写入map[string]string{...}--- 字面量创建,直接初始化ages["Alice"] = 25--- 添加或更新键值对ages["Unknown"]返回 0 --- 不存在的键返回值类型的零值,不会 panic- 这就是为什么需要 comma-ok 模式:
value, ok := ages["Bob"],ok=true表示键存在 - 如果只用
ages["Bob"],无法区分"Bob的年龄是0"和"Bob不存在" delete(ages, "Alice")--- 删除键值对,键不存在时 delete 不报错var nilMap map[string]int--- nil map,读取返回零值,但写入会 panic- 必须用
make()或字面量初始化才能写入
2. Map 遍历 --- range 与排序遍历
Map 遍历顺序是随机的,需要有序遍历时必须先排序键:
go
package main
import (
"fmt"
"sort"
)
func main() {
grades := map[string]int{
"Alice": 92, "Bob": 85, "Charlie": 78, "Diana": 96, "Eve": 88,
}
inventory := map[string]int{
"apples": 50, "bananas": 30, "oranges": 25,
}
// 键 + 值遍历(顺序随机!)
fmt.Println("Student grades:")
for name, grade := range grades {
fmt.Printf(" %s: %d\n", name, grade)
}
// 只遍历键
fmt.Println("\nStudent names (keys only):")
for name := range grades {
fmt.Printf(" %s\n", name)
}
// 只遍历值
fmt.Println("\nGrade values only:")
for _, grade := range grades {
fmt.Printf(" %d\n", grade)
}
// 求和与平均值
var sum int
var count int
for _, grade := range grades {
sum += grade
count++
}
average := float64(sum) / float64(count)
fmt.Printf("\nTotal sum: %d, Average: %.2f\n", sum, average)
// 找最高分
var maxGrade int
var topStudent string
for name, grade := range grades {
if grade > maxGrade {
maxGrade = grade
topStudent = name
}
}
fmt.Printf("Top student: %s with grade %d\n", topStudent, maxGrade)
// 有序遍历:先收集键 → 排序 → 按排序后的键访问
var keys []string
for product := range inventory {
keys = append(keys, product)
}
sort.Strings(keys)
fmt.Println("\nInventory (sorted by product name):")
for _, product := range keys {
fmt.Printf(" %s: %d\n", product, inventory[product])
}
// 条件计数
lowStock := 0
threshold := 30
for _, stock := range inventory {
if stock <= threshold {
lowStock++
}
}
fmt.Printf("\nProducts with low stock (<= %d): %d\n", threshold, lowStock)
}
执行结果:
Student grades:
Alice: 92
Bob: 85
Charlie: 78
Diana: 96
Eve: 88
Student names (keys only):
Charlie
Diana
Eve
Alice
Bob
Grade values only:
92
85
78
96
88
Total sum: 439, Average: 87.80
Top student: Diana with grade 96
Inventory (sorted by product name):
apples: 50
bananas: 30
oranges: 25
Products with low stock (<= 30): 2
要点:
for name, grade := range grades--- 遍历顺序不确定,每次运行可能不同for name := range grades--- 只遍历键(name 是键,不是值!)for _, grade := range grades---_忽略键,只遍历值- 有序遍历模式:收集键 →
sort.Strings(keys)→ 按排序后的键访问inventory[product] - 这是 Go 中 map 有序遍历的标准模式(Go 没有内置的有序 map)
- 统计计算:求和、平均值、最大值、条件计数,与切片遍历逻辑一致
3. Map 模式 --- 计数器(词频统计)
map[key]++ 是最常用的计数模式,零值特性让代码极简:
go
package main
import (
"fmt"
"strings"
)
func main() {
text := "hello world hello go programming go is awesome"
// 词频计数:map[string]int + wordCounts[word]++
wordCounts := make(map[string]int)
words := strings.Fields(text) // 按空格分割字符串
for _, word := range words {
wordCounts[word]++ // 关键!不需要判断键是否存在
// 不存在时 wordCounts[word] = 0(零值),++ 后变成 1
// 已存在时 wordCounts[word] 直接 ++ 增加
}
fmt.Println("Word counts:")
for word, count := range wordCounts {
fmt.Printf(" %s: %d\n", word, count)
}
}
执行结果:
Word counts:
hello: 2
world: 1
go: 2
programming: 1
is: 1
awesome: 1
要点:
wordCounts[word]++--- 这是 Go map 计数的核心技巧:- 首次遇到某词:
wordCounts["hello"]返回零值 0,++后变成 1 - 再次遇到:
wordCounts["hello"]= 1,++后变成 2 - 不需要判断键是否存在,零值特性自动处理
- 首次遇到某词:
strings.Fields(text)--- 按空格分割字符串为切片,比strings.Split(text, " ")更智能(处理多个连续空格)- 这是 Go 中最常用的 map 模式:统计频率、计数、投票等
4. Map 模式 --- 分组(Group by)
map[key][]value 实现分组:同一键的值追加到切片:
go
package main
import "fmt"
func main() {
students := []struct {
Name string
Grade string
Score int
}{
{"Alice", "A", 95}, {"Bob", "B", 87}, {"Charlie", "A", 92},
{"Diana", "B", 89}, {"Eve", "A", 98}, {"Frank", "C", 75}, {"Grace", "B", 85},
}
// 按等级分组:map[string][]string
gradeGroups := make(map[string][]string)
for _, student := range students {
gradeGroups[student.Grade] = append(gradeGroups[student.Grade], student.Name)
}
fmt.Println("Students grouped by grade:")
for grade, names := range gradeGroups {
fmt.Printf(" Grade %s: %v\n", grade, names)
}
}
执行结果:
Students grouped by grade:
Grade A: [Alice Charlie Eve]
Grade B: [Bob Diana Grace]
Grade C: [Frank]
要点:
make(map[string][]string)--- 值是切片类型,实现"一个键对应多个值"append(gradeGroups[student.Grade], student.Name)--- 零值是 nil 切片,append 自动分配底层数组- 首次遇到 Grade "A":
gradeGroups["A"]是 nil 切片,append(nil, "Alice")→ "Alice" - 再次遇到 Grade "A":
append(["Alice"], "Charlie")→ "Alice", "Charlie" - 不需要初始化空切片,nil 切片 append 是安全的
- 首次遇到 Grade "A":
- 分组是数据分析的基本操作:按部门分组员工、按日期分组事件等
5. Map 模式 --- 集合操作(Set)
Go 没有 Set 类型,用 map[key]bool 或 map[key]struct{} 模拟集合:
go
package main
import "fmt"
func main() {
set1 := map[string]bool{"apple": true, "banana": true, "cherry": true}
set2 := map[string]bool{"banana": true, "date": true, "cherry": true}
// 交集:在 set1 且在 set2 中
intersection := make(map[string]bool)
for key := range set1 {
if set2[key] { // set2[key] 返回 true = 存在,false = 不存在
intersection[key] = true
}
}
// 并集:所有元素
union := make(map[string]bool)
for key := range set1 { union[key] = true }
for key := range set2 { union[key] = true }
// 差集:在 set1 但不在 set2 中
difference := make(map[string]bool)
for key := range set1 {
if !set2[key] { // 不在 set2 中
difference[key] = true
}
}
fmt.Println("Set operations:")
fmt.Printf("Set1: %v\n", getKeys(set1))
fmt.Printf("Set2: %v\n", getKeys(set2))
fmt.Printf("Intersection: %v\n", getKeys(intersection))
fmt.Printf("Union: %v\n", getKeys(union))
fmt.Printf("Difference (set1 - set2): %v\n", getKeys(difference))
}
func getKeys(m map[string]bool) []string {
var keys []string
for key := range m { keys = append(keys, key) }
return keys
}
执行结果:
Set operations:
Set1: [apple banana cherry]
Set2: [banana date cherry]
Intersection: [banana cherry]
Union: [banana cherry date apple]
Difference (set1 - set2): [apple]
要点:
-
map[string]bool--- bool 值固定为 true,键本身就是集合元素 -
set2[key]--- 存在返回 true,不存在返回 false(零值),天然适合集合的"包含"判断 -
交集:遍历 set1,检查键是否也在 set2 中
-
并集:把两个 map 的所有键加入新 map(重复键自动合并)
-
差集:遍历 set1,排除在 set2 中存在的键
-
更节省内存的写法:
map[string]struct{}---struct{}占0字节,但判断存在需要 comma-ok:go_, exists := set[key] // 用 comma-ok 而非零值判断
6. Map 模式 --- 缓存/记忆化(Memoization)
map 作为缓存,避免重复计算:
go
package main
import "fmt"
func fibonacciMemo(n int, cache map[int]int) int {
if n <= 1 {
return n
}
// 先查缓存
if value, exists := cache[n]; exists {
return value // 已计算过,直接返回
}
// 未缓存,递归计算并存入缓存
result := fibonacciMemo(n-1, cache) + fibonacciMemo(n-2, cache)
cache[n] = result
return result
}
func main() {
fibCache := make(map[int]int)
fmt.Println("Fibonacci with memoization:")
for i := 0; i <= 10; i++ {
result := fibonacciMemo(i, fibCache)
fmt.Printf("fib(%d) = %d\n", i, result)
}
fmt.Printf("Cache after calculations: %v\n", fibCache)
}
执行结果:
Fibonacci with memoization:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34
fib(10) = 55
Cache after calculations: map[2:1 3:2 4:3 5:5 6:8 7:13 8:21 9:34 10:55]
要点:
cache map[int]int--- 参数缓存,n 为键,fib(n) 为值if value, exists := cache[n]; exists--- 先查缓存,存在则直接返回(避免重复计算)cache[n] = result--- 计算后存入缓存,下次相同 n 不再递归- 对比朴素递归:O(2^n) → 记忆化后 O(n),性能大幅提升
- 缓存中存储了 fib(2) 到 fib(10) 的结果(0 和 1 是基例,未存缓存)
- 记忆化是优化递归的通用技术,适用于任何有重复子问题的算法
7. Map 模式 --- 默认值与频率分析
go
package main
import "fmt"
// 获取配置值,不存在时返回默认值
func getConfigValue(config map[string]string, key, defaultValue string) string {
if value, exists := config[key]; exists {
return value
}
return defaultValue
}
func main() {
// 默认值模式
userConfig := map[string]string{"theme": "dark", "lang": "en"}
theme := getConfigValue(userConfig, "theme", "light") // 存在,返回 "dark"
language := getConfigValue(userConfig, "lang", "en") // 存在,返回 "en"
timeout := getConfigValue(userConfig, "timeout", "30s") // 不存在,返回默认 "30s"
fmt.Printf("Theme: %s\n", theme)
fmt.Printf("Language: %s\n", language)
fmt.Printf("Timeout: %s (default used)\n", timeout)
// 频率分析:找最频繁的数
numbers := []int{1, 2, 3, 2, 1, 3, 1, 4, 5, 4, 1}
frequency := make(map[int]int)
for _, num := range numbers {
frequency[num]++ // 计数器模式
}
var mostFrequent int
var maxCount int
for num, count := range frequency {
if count > maxCount {
mostFrequent = num
maxCount = count
}
}
fmt.Printf("\nMost frequent number: %d (appears %d times)\n", mostFrequent, maxCount)
}
执行结果:
Theme: dark
Language: en
Timeout: 30s (default used)
Most frequent number: 1 (appears 4 times)
要点:
- 默认值模式:
getConfigValue(config, key, default)--- 用 comma-ok 检查键是否存在,不存在返回默认值 - 频率分析 = 计数器模式 + 找最大值:先用
frequency[num]++统计频次,再遍历 map 找最高频 - 1 出现 4 次(最多),2 出现 2 次,3 出现 2 次,4 出现 2 次,5 出现 1 次
- 实际场景:配置管理、最热商品、最活跃用户等
8. Map 模式 --- 嵌套 map(Map of Maps)
map[key1]map[key2]value 实现二维数据结构:
go
package main
import "fmt"
func main() {
// 嵌套 map:学生 → 科目 → 成绩
studentGrades := make(map[string]map[string]int)
// 必须初始化每个内层 map!
studentGrades["Alice"] = make(map[string]int)
studentGrades["Bob"] = make(map[string]int)
studentGrades["Alice"]["Math"] = 95
studentGrades["Alice"]["Science"] = 92
studentGrades["Alice"]["English"] = 88
studentGrades["Bob"]["Math"] = 87
studentGrades["Bob"]["Science"] = 90
studentGrades["Bob"]["English"] = 85
// 嵌套 range 遍历
fmt.Println("Student grades by subject:")
for student, subjects := range studentGrades {
fmt.Printf("%s:\n", student)
for subject, grade := range subjects {
fmt.Printf(" %s: %d\n", subject, grade)
}
}
}
执行结果:
Student grades by subject:
Bob:
Science: 90
English: 85
Math: 87
Alice:
Math: 95
Science: 92
English: 88
要点:
make(map[string]map[string]int)--- 外层 map 的值是内层 map- 必须手动初始化内层 map :
studentGrades["Alice"] = make(map[string]int)- 如果不初始化:
studentGrades["Alice"]["Math"] = 95会 panic(内层 map 是 nil)
- 如果不初始化:
- 嵌套 range 遍历:外层遍历学生,内层遍历科目和成绩
- 嵌套 map 适合多维度数据:学生×科目、城市×月份、产品×属性等
- 注意:Go 的 map 不能直接做 map 的值类型字面量初始化,必须逐层 make
知识点总结
| 知识点 | 关键概念 |
|---|---|
| Map 创建 | make(map[K]V) 或 map[K]V{...} 字面量 |
| 访问与零值 | 不存在的键返回零值,用 comma-ok 区分"零值"和"不存在" |
| nil map | var 声明是 nil,读取返回零值,写入 panic |
| delete | delete(map, key) 删除键,不存在不报错 |
| 遍历 | range 顺序随机,有序遍历需先排序键 |
| 计数器 | map[key]++ 零值自动为0,不需要判断键是否存在 |
| 分组 | map[K][]V 同一键的值追加到切片,nil切片append安全 |
| 集合 | map[K]bool 模拟 Set,支持交集/并集/差集 |
| 缓存/记忆化 | map 存储计算结果,避免重复计算 |
| 默认值 | comma-ok 检查键,不存在返回默认值 |
| 嵌套 map | map[K1]map[K2]V 必须逐层 make 初始化内层 |