244 lines
7.3 KiB
Markdown
244 lines
7.3 KiB
Markdown
# 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)
|
||
}
|
||
}
|
||
```
|