ソースを参照

feat: 新增第六课map键值对例程及学习说明

zhuts 4 日 前
親
コミット
2015c5e114
3 ファイル変更、242 行追加、3 行削除
  1. 7 3
      README.md
  2. 156 0
      lessons/06_maps/README.md
  3. 79 0
      lessons/06_maps/main.go

+ 7 - 3
README.md

@@ -24,9 +24,12 @@
     ├── 04_loops/           # 第 04 课:for 循环
     │   ├── README.md       # 循环语法、编译运行与练习
     │   └── main.go         # 累加、跳过、退出和字符串遍历
-    └── 05_arrays_slices/   # 第 05 课:数组与切片
-        ├── README.md       # 固定长度、追加、共享和复制
-        └── main.go         # 带逐节注释的参考代码
+    ├── 05_arrays_slices/   # 第 05 课:数组与切片
+    │   ├── README.md       # 固定长度、追加、共享和复制
+    │   └── main.go         # 带逐节注释的参考代码
+    └── 06_maps/            # 第 06 课:map 键值对
+        ├── README.md       # 增删改查、遍历、nil 与共享
+        └── main.go         # 按姓名查询成绩的参考代码
 ```
 
 根目录不放例程代码。各课共享根目录的 `go.mod`,不需要逐课初始化模块,也不需要 `go.work`。当前模块路径 `example.com/go-study` 是学习用标识,不必与 Git 地址相同。
@@ -40,6 +43,7 @@
 | 03 | [if 条件判断](./lessons/03_conditions/README.md) | 条件分支、比较、逻辑运算 | 已建立 |
 | 04 | [for 循环](./lessons/04_loops/README.md) | 三段式、条件循环、break、continue、range | 已建立 |
 | 05 | [数组与切片](./lessons/05_arrays_slices/README.md) | 索引、遍历、append、截取、make、copy | 已建立 |
+| 06 | [map 键值对](./lessons/06_maps/README.md) | 增删改查、存在性判断、遍历、nil | 已建立 |
 
 后续按编号逐步新增独立目录。不一次性铺开后续课程,也不把新例程写进旧例程的入口文件。
 

+ 156 - 0
lessons/06_maps/README.md

@@ -0,0 +1,156 @@
+# 第 06 课:map 键值对
+
+## 学习目标
+
+数组与切片通过数字索引访问数据,map 则通过“键”查找“值”。本课用姓名查成绩,学习增删改查、判断键是否存在、遍历及 nil map。
+
+按 [main.go](./main.go) 中的 1~7 节逐段手敲。本课共享根目录的 `go.mod`,仅使用标准库,不需要新增依赖。
+
+## 1. 声明和创建
+
+```go
+scores := map[string]int{
+    "小朱": 90,
+    "小李": 75,
+}
+```
+
+`map[string]int` 表示键是 `string`,值是 `int`。键唯一,给已有键赋值会覆盖原值,不会产生第二份同名记录。
+
+多行复合字面量中,最后一项后面也需要逗号。
+
+另一种方式是先创建空 map,再写入:
+
+```go
+scores := make(map[string]int)
+scores["小朱"] = 90
+```
+
+map 的键必须是可比较类型;本课先使用字符串。整数也可以做键,切片和 map 本身不能做键。
+
+## 2. 增删改查
+
+| 操作 | 示例 |
+| --- | --- |
+| 新增或修改 | `scores["小李"] = 80` |
+| 读取 | `score := scores["小李"]` |
+| 判断是否存在 | `score, ok := scores["小李"]` |
+| 删除 | `delete(scores, "小李")` |
+| 元素数量 | `len(scores)` |
+
+读取不存在的键会得到值类型的零值,**不会自动创建这个键**。因此值为 `0` 不代表键不存在,需要用 `ok` 判断。
+
+删除不存在的键不会报错。map 没有切片那样的 `append`,新增直接通过键赋值,也不能用 `cap` 查询容量。
+
+## 3. if 初始化语句
+
+```go
+if score, ok := scores["小李"]; ok {
+    fmt.Println(score)
+} else {
+    fmt.Println("没有找到")
+}
+```
+
+分号前是查询并声明变量,分号后才是布尔条件。这里的 `score`、`ok` 只在整个 if / else 语句内有效,不会泄漏到语句外。
+
+## 4. 遍历顺序不固定
+
+```go
+for name, score := range scores {
+    fmt.Println(name, score)
+}
+```
+
+不能依赖 map 的插入顺序,也不能假设连续两次遍历顺序相同。本例直接遍历时只做总分统计,不依赖顺序。
+
+如果需要按指定顺序显示,可以准备一个姓名切片逐项查询;如果需要自动排序,可在后续学习标准库 `sort` 后先排序键。直接打印整个 map 的表现不能代替对 range 顺序的理解。
+
+## 5. nil map 与共享
+
+```go
+var scores map[string]int
+```
+
+此时 `scores == nil`,长度为 `0`。读取得到零值,查询存在性得到 `false`,delete 也安全,但直接写入会触发运行时 panic。应先使用 `make` 或 map 字面量初始化。
+
+```go
+alias := scores
+```
+
+对已初始化的 map 做这样的赋值不会独立复制元素,通过 alias 修改条目会影响 scores。需要独立副本时,应新建 map 并循环复制;未来涉及切片或指针值时还要区分浅拷贝和深拷贝。
+
+## 编译并运行
+
+在**仓库根目录**执行:
+
+```powershell
+New-Item -ItemType Directory -Force bin/06_maps | Out-Null
+go build -o ./bin/06_maps/maps.exe ./lessons/06_maps
+.\bin\06_maps\maps.exe
+```
+
+修改源码后需要重新编译。快速编译并运行:
+
+```powershell
+go run ./lessons/06_maps
+```
+
+也可以从根目录进入本课:
+
+```powershell
+Set-Location lessons/06_maps
+go run .
+```
+
+## 预期输出
+
+```text
+=== 创建与查询 ===
+学生数量:3
+小朱的成绩:90
+
+=== 新增与修改 ===
+新增小陈:88,修改小李:80
+学生数量:4
+
+=== 判断键是否存在 ===
+直接查询小赵:0
+查询小王:成绩=0,存在=true
+查询小赵:成绩=0,存在=false
+找到小李,成绩:80
+
+=== 删除 ===
+删除后小王存在:false,学生数量:3
+
+=== 遍历与统计 ===
+总分:258
+平均分:86.00
+小朱:90
+小李:80
+小陈:88
+
+=== nil 与 make ===
+初始化前:nil=true,长度=0,读取结果=false
+初始化后:nil=false,复习完成=true
+
+=== 赋值与共享 ===
+通过 alias 修改后,小朱的原成绩也变为:95
+```
+
+## 动手练习
+
+- [ ] 新增一个学生,检查 `len` 和总分如何变化。
+- [ ] 查询成绩为 0 的学生和不存在的学生,比较 `ok`。
+- [ ] 把 if 查询的姓名改成不存在的姓名,观察 else 分支。
+- [ ] 自己写一次 `for name, score := range scores` 输出,理解顺序不保证固定。
+- [ ] 删除一个键后,用 `_, ok := ...` 检查是否还存在。
+- [ ] 创建空 map,循环复制 scores,修改副本后验证原 map 不受影响。
+
+增加或删除学生后,固定姓名切片不会自动更新,需要同步调整。默认代码在修改 alias 前已经统计过总分,所以修改发生后,如需新总分,应重新计算。
+
+若要另外手敲,放进本课 `practice/` 子目录,用 `go run ./lessons/06_maps/practice` 运行,不要在同一个包里重复声明 main。
+
+下一课建议正式学习函数:参数、返回值与多返回值,把计算逻辑拆分成可复用的小块。
+
+[上一课:数组与切片](../05_arrays_slices/README.md) · [学习总览](../../README.md)

+ 79 - 0
lessons/06_maps/main.go

@@ -0,0 +1,79 @@
+package main
+
+import "fmt"
+
+func main() {
+	// 1. map[键类型]值类型:这里用姓名 string 查找成绩 int。
+	fmt.Println("=== 创建与查询 ===")
+	scores := map[string]int{
+		"小朱": 90,
+		"小李": 75,
+		"小王": 0,
+	}
+	fmt.Printf("学生数量:%d\n", len(scores))
+	fmt.Printf("小朱的成绩:%d\n", scores["小朱"])
+
+	// 2. 新键是新增,已有键是修改;同一个 map 的键唯一。
+	fmt.Println("\n=== 新增与修改 ===")
+	scores["小陈"] = 88
+	scores["小李"] = 80
+	fmt.Printf("新增小陈:%d,修改小李:%d\n", scores["小陈"], scores["小李"])
+	fmt.Printf("学生数量:%d\n", len(scores))
+
+	// 3. 读取不存在的键会得到值类型的零值,不会自动新增这个键。
+	// 仅凭 0 无法区分“成绩是 0”和“没有这个学生”,需要第二个返回值 ok。
+	fmt.Println("\n=== 判断键是否存在 ===")
+	fmt.Printf("直接查询小赵:%d\n", scores["小赵"])
+	score, ok := scores["小王"]
+	fmt.Printf("查询小王:成绩=%d,存在=%t\n", score, ok)
+	score, ok = scores["小赵"] // 变量已声明,这里用 = 赋值。
+	fmt.Printf("查询小赵:成绩=%d,存在=%t\n", score, ok)
+
+	// 也可以把查询放在 if 的初始化语句中。
+	// value 和 exists 只在这个 if / else 语句内有效。
+	if value, exists := scores["小李"]; exists {
+		fmt.Printf("找到小李,成绩:%d\n", value)
+	} else {
+		fmt.Println("没有找到小李")
+	}
+
+	// 4. delete 删除键;删除不存在的键也不会报错。
+	fmt.Println("\n=== 删除 ===")
+	delete(scores, "小王")
+	delete(scores, "不存在的学生")
+	_, ok = scores["小王"]
+	fmt.Printf("删除后小王存在:%t,学生数量:%d\n", ok, len(scores))
+
+	// 5. range 遍历 map 的顺序不保证固定,不能依赖插入顺序。
+	// 这里只做求和,不按遍历顺序输出,因此结果稳定。
+	fmt.Println("\n=== 遍历与统计 ===")
+	total := 0
+	for _, value := range scores {
+		total += value
+	}
+	fmt.Printf("总分:%d\n", total)
+	if len(scores) > 0 {
+		average := float64(total) / float64(len(scores))
+		fmt.Printf("平均分:%.2f\n", average)
+	}
+
+	// 要固定显示顺序,可按一个明确的姓名切片逐项查询。
+	for _, name := range []string{"小朱", "小李", "小陈"} {
+		fmt.Printf("%s:%d\n", name, scores[name])
+	}
+
+	// 6. map 的零值是 nil:可以读取、查询、删除,但不能直接写入。
+	fmt.Println("\n=== nil 与 make ===")
+	var tasks map[string]bool
+	fmt.Printf("初始化前:nil=%t,长度=%d,读取结果=%t\n", tasks == nil, len(tasks), tasks["复习"])
+	// tasks["复习"] = true // 错误:向 nil map 写入会发生运行时 panic。
+	tasks = make(map[string]bool)
+	tasks["复习"] = true
+	fmt.Printf("初始化后:nil=%t,复习完成=%t\n", tasks == nil, tasks["复习"])
+
+	// 7. map 赋值不会复制所有元素,两个变量会指向同一份 map 数据。
+	fmt.Println("\n=== 赋值与共享 ===")
+	alias := scores
+	alias["小朱"] = 95
+	fmt.Printf("通过 alias 修改后,小朱的原成绩也变为:%d\n", scores["小朱"])
+}