Files
notes/resource/go/Go 语言的映射(map).md
2026-03-01 01:43:46 +08:00

244 lines
7.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 1. Map 的本质与内部实现
Map 是 Go 语言中一种核心的数据结构,它用于存储键值对(key-value)的无序集合。
- **本质**: 它在其他语言中通常被称为哈希表(Hash Table)或字典(Dictionary)。
- **内部实现**: Go 的 map 底层是通过哈希表实现的。通过一个哈希函数,将键(key)计算成一个哈希值,然后根据这个哈希值定位到存储桶(bucket)来存放对应的值(value)。这使得 map 在平均情况下的**增、删、查操作的时间复杂度都能达到 O(1)**。
# 2. Map 的创建与初始化
创建 map 是使用它的第一步,不同的创建方式适用于不同的场景。
## 方式 1:使用 `make` 函数(推荐)
这是最常用的方式。可以指定类型,并可选地指定初始容量。
```go
package main
import "fmt"
func main() {
// 使用 make 创建一个空的 map
// 此时 m2 不是 nil,是一个长度为 0 的 map,可以直接使用
m2 := make(map[string]int)
m2["math"] = 90
fmt.Printf("m2: %#v, len: %d\n", m2, len(m2)) // 输出: m2: map[string]int{"math":90}, len: 1
// [补充知识点] 预估容量可以提高性能,避免后续操作中的内存重新分配
// 创建一个容量为 10 的 map,但当前长度仍为 0
m3 := make(map[string]int, 10)
fmt.Printf("m3: %#v, len: %d\n", m3, len(m3)) // 输出: m3: map[string]int{}, len: 0
}
```
## 方式 2:使用字面量 literal 初始化
如果在创建时就能确定一些初始键值对,这种方式非常直观。
```go
package main
import "fmt"
func main() {
// 在声明时直接提供初始键值对
scores := map[string]int{
"张三": 95,
"李四": 88,
"王五": 79,
}
fmt.Printf("scores: %#v, len: %d\n", scores, len(scores))
}
```
## 方式 3:仅声明(**注意陷阱**)
这种方式会创建一个 `nil map`,它不能直接用于存储键值对,否则会引发 `panic`
```go
package main
import "fmt"
func main() {
// 仅声明一个 map 变量,其零值为 nil
var m1 map[string]int
fmt.Printf("m1 is nil: %v\n", m1 == nil) // 输出: m1 is nil: true
// 对 nil map 进行读操作是安全的,会返回零值
score := m1["张三"]
fmt.Printf("从 nil map 读取 '张三' 的分数: %d\n", score) // 输出: 0
// !!! 错误操作:对 nil map 进行写操作会引发 panic
// m1["张三"] = 100 // a panic: assignment to entry in nil map
// 正确做法:在使用前必须先用 make 进行初始化
m1 = make(map[string]int)
m1["张三"] = 100
fmt.Printf("初始化后: %#v\n", m1)
}
```
# 3. Map 的核心操作
Map 的基本操作包括增加/更新、查询、删除和判断键是否存在。
```go
package main
import "fmt"
func main() {
scores := make(map[string]int)
// 1. 增加/更新元素
// 如果键 "小明" 不存在,则为增加;如果已存在,则为更新
scores["小明"] = 90
fmt.Println("增加后:", scores)
scores["小明"] = 100 // 更新
fmt.Println("更新后:", scores)
// 2. 获取元素
// 直接获取,如果键不存在,会得到该值类型的零值(对于 int 是 0)
score := scores["小明"]
fmt.Println("小明的分数:", score)
scoreUnknown := scores["小红"] // "小红" 不存在
fmt.Println("小红的分数 (零值):", scoreUnknown)
// 3. 判断键是否存在("comma ok" idiom
// 这是判断键是否存在的标准方式,推荐使用
score, exists := scores["小明"]
if exists {
fmt.Printf("小明的分数存在: %d\n", score)
} else {
fmt.Println("小明的分数不存在")
}
_, exists = scores["小红"] // 可以忽略第一个返回值(值本身)
if !exists {
fmt.Println("小红的分数不存在")
}
// 4. 删除元素
// 使用 delete() 内置函数,如果键不存在,此操作不会产生任何效果,也不会报错
delete(scores, "小明")
fmt.Println("删除后:", scores)
delete(scores, "不存在的键") // 安全操作
}
```
# 4. 遍历 Map
使用 `for…range` 循环可以遍历 map 的所有键值对。
**重要**:Map 的遍历顺序是**随机**的,Go 语言在设计上特意如此,以防止开发者依赖于某个固定的遍历顺序。
```go
package main
import "fmt"
func main() {
scores := map[string]int{
"张三": 95,
"李四": 88,
"王五": 79,
"赵六": 92,
}
fmt.Println("--- 遍历键值对 ---")
// 每次运行的输出顺序可能不同
for name, score := range scores {
fmt.Printf("姓名: %s, 分数: %d\n", name, score)
}
fmt.Println("\n--- 只遍历键 ---")
for name := range scores {
fmt.Printf("姓名: %s\n", name)
}
// [补充知识点] 如果需要有序遍历,需要先提取所有的键,对键进行排序,然后根据排序后的键进行遍历。
// 这部分内容将在排序相关章节详细讲解。
}
```
# 5. 核心特性与常见陷阱 (重点)
## 5.1 Map 是引用类型
将 map 变量赋值给另一个变量,或作为函数参数传递时,它们都指向**同一个**底层数据结构。修改其中一个会影响到另一个。
```go
package main
import "fmt"
// modifyMap 函数会修改传入的 map
func modifyMap(m map[string]int) {
m["extra"] = 1000
}
func main() {
originalMap := map[string]int{"a": 1, "b": 2}
fmt.Println("原始 map:", originalMap)
// 将 map 作为参数传递
modifyMap(originalMap)
// originalMap 的内容被修改了
fmt.Println("函数调用后的 map:", originalMap)
}
```
## 5.2 键的类型限制
Map 的键(key)必须是**可比较的**类型,否则会在编译时报错。
- **可比较的类型**:布尔型、数字类型、字符串、指针、通道、接口,以及只包含可比较字段的结构体和数组。
- **不可比较的类型**:切片(slice)、map、函数。这些类型不能作为 map 的键。
## 5.3 并发不安全
Go 内置的 map 类型**不是并发安全的**。如果在多个 goroutine 中同时对一个 map 进行读写操作,而不加任何同步控制,会导致程序崩溃(panic)。
- **解决方案**: 若需要在并发环境中使用 map,应该使用 `sync.Mutex``sync.RWMutex` 进行加锁保护,或者使用 Go 1.9 之后提供的 `sync.Map` 类型。`sync.Map` 专门为“读多写少”的并发场景进行了优化。
# 6. 实战示例:统计字符串中各字符出现的次数
这是一个非常经典的 map 应用场景,您的笔记中提供的示例已经很好了,我这里将其格式化为一个更完整的可运行程序,并添加了更详细的注释。
```go
package main
import "fmt"
// countChars 函数接收一个字符串,返回一个 map,记录每个字符出现的次数。
// 使用 rune 类型作为键,可以正确处理多字节的 Unicode 字符(如汉字)。
func countChars(s string) map[rune]int {
// 创建一个 map 用于存储结果
charCount := make(map[rune]int)
// 遍历字符串中的每一个字符 (rune)
for _, char := range s {
// map 的一个优雅之处:如果键不存在,map[key] 的访问会返回零值(int 的零值是 0)
// 所以可以直接进行 ++ 操作,无需预先检查键是否存在。
charCount[char]++
}
return charCount
}
func main() {
text := "Go语言, a great language!"
result := countChars(text)
fmt.Printf("字符串 \"%s\" 的字符统计结果如下:\n", text)
for char, count := range result {
// 使用 %c 格式化字符,%d 格式化次数
fmt.Printf("字符 '%c' 出现了 %d 次\n", char, count)
}
}
```