# 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) } } ```