7.3 KiB
7.3 KiB
1. Map 的本质与内部实现
Map 是 Go 语言中一种核心的数据结构,它用于存储键值对(key-value)的无序集合。
- 本质: 它在其他语言中通常被称为哈希表(Hash Table)或字典(Dictionary)。
- 内部实现: Go 的 map 底层是通过哈希表实现的。通过一个哈希函数,将键(key)计算成一个哈希值,然后根据这个哈希值定位到存储桶(bucket)来存放对应的值(value)。这使得 map 在平均情况下的增、删、查操作的时间复杂度都能达到 O(1)。
2. Map 的创建与初始化
创建 map 是使用它的第一步,不同的创建方式适用于不同的场景。
方式 1:使用 make 函数(推荐)
这是最常用的方式。可以指定类型,并可选地指定初始容量。
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 初始化
如果在创建时就能确定一些初始键值对,这种方式非常直观。
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。
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 的基本操作包括增加/更新、查询、删除和判断键是否存在。
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 语言在设计上特意如此,以防止开发者依赖于某个固定的遍历顺序。
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 变量赋值给另一个变量,或作为函数参数传递时,它们都指向同一个底层数据结构。修改其中一个会影响到另一个。
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 应用场景,您的笔记中提供的示例已经很好了,我这里将其格式化为一个更完整的可运行程序,并添加了更详细的注释。
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)
}
}