6·基础入门入门

Map 哈希表

mapkey-valuedelete

第 6 课:Map 哈希表

所属模块: 基础入门 难度: beginner 标签: map, key-value, delete 上一课: 数组与切片 下一课: 字符串与 rune


1. 学习目标

完成本课学习后,你将能够:

  1. 理解 Map(映射)数据结构的概念及其在 Go 中的作用。
  2. 掌握 Map 的声明、初始化、添加、访问、修改和删除元素的操作。
  3. 学会使用 for range 遍历 Map 中的键值对。
  4. 了解 Map 的零值、nil Map 的概念及其使用注意事项。
  5. 特别提醒:认识到 Map 在 Go 中不是并发安全的,为后续并发编程课程做铺垫。

2. 核心概念

什么是 Map?

想象一下你的手机通讯录或一本字典:你通过一个唯一的标识(名字或单词)来查找对应的信息(电话号码或释义)。Map(映射) 在 Go 语言中就是这样一个存储 “键-值对” 的数据结构。你提供一个 Key(键),Map 会告诉你对应的 Value(值) 是什么。

  • 键(Key):必须是可比较的类型(如 string, int, bool, 指针等),不能是 slicemapfunc
  • 值(Value):可以是任意类型,包括另一个 Map 或 Slice。

声明与初始化

  • 声明一个 nil Mapvar m map[string]int。此时 mnil,对它进行添加操作会引发 panic(恐慌),必须先初始化。
  • 使用字面量初始化m := map[string]int{“apple”: 5, “banana”: 3}
  • 使用 make 函数初始化m := make(map[string]int)。这是创建空 Map 的推荐方式。

核心操作

  • 添加/修改m[key] = value。如果键已存在,则修改其值;如果键不存在,则添加一对新键值。
  • 访问value, ok := m[key]ok 是一个布尔值,表示键是否存在。强烈建议总是检查 ok,以避免获得零值带来的误解。
  • 删除delete(m, key)。如果键不存在,此操作无副作用,不会报错。
  • 获取长度len(m) 返回 Map 中键值对的数量。

3. 代码示例

以下是一个完整的、可运行的示例,演示了 Map 的基本操作:

package main

import "fmt"

func main() {
    // 1. 使用 make 初始化一个空 Map
    studentScores := make(map[string]int)
    fmt.Println("初始 Map:", studentScores, "长度:", len(studentScores))

    // 2. 添加键值对
    studentScores["Alice"] = 95
    studentScores["Bob"] = 88
    studentScores["Charlie"] = 72
    fmt.Println("添加数据后:", studentScores, "长度:", len(studentScores))

    // 3. 访问元素(推荐方式:检查键是否存在)
    score, ok := studentScores["Alice"]
    if ok {
        fmt.Printf("Alice 的分数是: %d\n", score)
    } else {
        fmt.Println("未找到 Alice 的分数")
    }

    // 访问一个不存在的键
    _, ok = studentScores["Dave"]
    if !ok {
        fmt.Println("Dave 不在我们的成绩单中")
    }

    // 4. 修改元素
    studentScores["Bob"] = 90 // 将 Bob 的分数从 88 改为 90
    fmt.Println("修改 Bob 的分数后:", studentScores)

    // 5. 删除元素
    delete(studentScores, "Charlie")
    fmt.Println("删除 Charlie 后:", studentScores, "长度:", len(studentScores))

    // 6. 使用字面量初始化并遍历 Map
    capitals := map[string]string{
        "China":  "Beijing",
        "USA":    "Washington D.C.",
        "Japan":  "Tokyo",
        "France": "Paris",
    }
    fmt.Println("\n世界首都:")
    for country, city := range capitals {
        fmt.Printf("%s 的首都是 %s\n", country, city)
    }
}

输出示例:

初始 Map: map[] 长度: 0
添加数据后: map[Alice:95 Bob:88 Charlie:72] 长度: 3
Alice 的分数是: 95
Dave 不在我们的成绩单中
修改 Bob 的分数后: map[Alice:95 Bob:90 Charlie:72]
删除 Charlie 后: map[Alice:95 Bob:90] 长度: 2

世界首都:
China 的首都是 Beijing
USA 的首都是 Washington D.C.
Japan 的首都是 Tokyo
France 的首都是 Paris

4. 实践练习

练习题 1:基础操作

创建一个 map[string][]string,键是编程语言的名字,值是存储该语言代表框架的字符串切片。完成以下操作:

  1. 初始化 Map 并添加 “Go”: [“Gin”, “Beego”], “Python”: [“Django”, “Flask”]。
  2. 访问并打印 “Python” 对应的框架列表。
  3. 为 “JavaScript” 添加一个键,其值为 [“React”, “Vue”]。
  4. 删除 “Go” 这个键。

预期输出:

Python 框架: [Django Flask]
Map 更新后: map[Go:[Gin Beego] JavaScript:[React Vue] Python:[Django Flask]]
删除后: map[JavaScript:[React Vue] Python:[Django Flask]]

练习题 2:字符频率统计

编写一个函数 countChars(s string) map[string]int,它接收一个字符串,返回一个 Map,其中键是字符串中出现的每个字符(转为 string 类型),值是该字符出现的次数。

  • 提示:可以使用 for _, c := range s 来遍历字符串中的字符(rune)。

示例调用与预期输出:

fmt.Println(countChars("hello"))
// 输出: map[h:1 e:1 l:2 o:1]

练习题 3:查找最大值(稍有挑战)

编写一个函数,接收一个 map[string]int 作为参数,找出并返回 Map 中值最大的那个

  • 如果 Map 为空,返回空字符串 ""

示例调用与预期输出:

scores := map[string]int{"Math": 90, "English": 85, "History": 95}
topSubject := findTopKey(scores)
fmt.Println(topSubject) // 输出: History

5. 常见错误

  1. nil Map 进行写操作

    var m map[string]int // m 是 nil
    m["key"] = 1 // 运行时错误:panic: assignment to entry in nil map
    

    正确做法:在使用前用 make 或字面量初始化。

  2. 忽略键值存在性检查

    m := make(map[string]int)
    value := m["missing_key"] // value 将是 0,你无法区分是“键不存在”还是“键的值本就是0”。
    

    正确做法:总是使用 value, ok := m[key] 的形式。

  3. 并发读写 Map(非常重要!): Go 的内建 Map 不是并发安全的。如果多个 Goroutine 同时读写一个 Map,程序会发生数据竞争,导致不可预测的行为甚至崩溃。

    // 危险!以下代码是错误示例!
    m := make(map[int]int)
    go func() { m[1] = 1 }() // Goroutine 1 写
    go func() { fmt.Println(m[1]) }() // Goroutine 2 读
    

    解决方法

    • 使用 sync.Mutexsync.RWMutex 对 Map 的访问进行加锁。
    • 使用 Go 1.9 引入的并发安全 Map:sync.Map。(这通常在更高级的并发课程中介绍)

6. 小结

  • Map 是存储键-值对的集合,键必须可比较。
  • 使用 make 或字面量初始化,切勿对 nil Map 进行写操作。
  • 核心操作:m[k]=v(添加/修改),v, ok:=m[k](访问,检查存在性),delete(m, k)(删除)。
  • 使用 for k, v := range m遍历 Map。
  • 最重要的警告:Go 的内建 Map 不是并发安全的!在多 Goroutine 环境下操作同一个 Map,必须自行实现同步机制(如加锁)或使用 sync.Map。这是 Go 并发编程中一个基础但关键的概念。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「字符串与 rune」 以巩固所学知识。