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

7.3 KiB
Raw Permalink Blame History

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.Mutexsync.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)
	}
}