5 커밋 50d08f1399 ... 888ec3e737

작성자 SHA1 메시지 날짜
  zhuts 888ec3e737 feat: 新增第九课指针例程与参数传递说明 3 일 전
  zhuts 5a0610ebbf feat: 新增第八课结构体例程与逐步学习说明 3 일 전
  zhuts aba9ec24e3 feat: 新增第七课函数例程与配套测试说明 3 일 전
  zhuts 2015c5e114 feat: 新增第六课map键值对例程及学习说明 4 일 전
  zhuts 9f7453cbb6 feat: 新增第五课数组与切片例程及学习说明 4 일 전

+ 24 - 3
README.md

@@ -21,9 +21,25 @@
     ├── 03_conditions/      # 第 03 课:if 条件判断
     │   ├── README.md       # 语法、编译运行与边界练习
     │   └── main.go         # 分支、比较和逻辑运算
-    └── 04_loops/           # 第 04 课:for 循环
-        ├── README.md       # 循环语法、编译运行与练习
-        └── main.go         # 累加、跳过、退出和字符串遍历
+    ├── 04_loops/           # 第 04 课:for 循环
+    │   ├── README.md       # 循环语法、编译运行与练习
+    │   └── main.go         # 累加、跳过、退出和字符串遍历
+    ├── 05_arrays_slices/   # 第 05 课:数组与切片
+    │   ├── README.md       # 固定长度、追加、共享和复制
+    │   └── main.go         # 带逐节注释的参考代码
+    ├── 06_maps/            # 第 06 课:map 键值对
+    │   ├── README.md       # 增删改查、遍历、nil 与共享
+    │   └── main.go         # 按姓名查询成绩的参考代码
+    ├── 07_functions/       # 第 07 课:函数
+    │   ├── README.md       # 参数、返回值、值传递和练习
+    │   ├── main.go         # 普通函数、多返回值、可变参数
+    │   └── main_test.go    # 本课函数测试
+    ├── 08_structs/         # 第 08 课:结构体
+    │   ├── README.md       # 定义、字段、复制和结构体切片
+    │   └── main.go         # 学生记录与简化测试数据表
+    └── 09_pointers/        # 第 09 课:指针
+        ├── README.md       # 取地址、解引用、nil 和参数传递
+        └── main.go         # 指针修改整数与结构体
 ```
 
 根目录不放例程代码。各课共享根目录的 `go.mod`,不需要逐课初始化模块,也不需要 `go.work`。当前模块路径 `example.com/go-study` 是学习用标识,不必与 Git 地址相同。
@@ -36,6 +52,11 @@
 | 02 | [变量、常量与基础类型](./lessons/02_variables/README.md) | 声明、赋值、零值、类型转换 | 已建立 |
 | 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 | 已建立 |
+| 07 | [函数](./lessons/07_functions/README.md) | 参数、返回值、值传递、多返回值、可变参数 | 已建立 |
+| 08 | [结构体](./lessons/08_structs/README.md) | 定义、字段访问、零值、复制、结构体切片 | 已建立 |
+| 09 | [指针](./lessons/09_pointers/README.md) | &、*、nil、指针参数、结构体指针、new | 已建立 |
 
 后续按编号逐步新增独立目录。不一次性铺开后续课程,也不把新例程写进旧例程的入口文件。
 

+ 157 - 0
lessons/05_arrays_slices/README.md

@@ -0,0 +1,157 @@
+# 第 05 课:数组与切片
+
+## 学习目标
+
+学会保存一组同类型数据,并使用上一课的循环遍历它们。按 [main.go](./main.go) 中的 1~7 节逐段手敲,不急着学习复杂内存管理。
+
+本课独立存放,共享根目录的 `go.mod`,仅使用标准库,无需安装第三方依赖。
+
+## 1. 数组:固定长度
+
+```go
+scores := [3]int{80, 90, 75}
+fmt.Println(scores[0]) // 第一个元素,值为 80。
+scores[2] = 85         // 修改第三个元素。
+```
+
+- `[3]int` 表示包含 3 个整数的数组,长度也是类型的一部分;`[3]int` 与 `[4]int` 是不同类型。
+- 索引从 `0` 开始,最后一个元素的索引是 `len(scores)-1`。
+- `var scores [3]int` 不给初始值时,三个元素都为 `0`。
+- 数组长度不能在运行时改变,越界访问会导致编译错误或运行时 panic。
+
+与 C 类似,可以通过索引访问数组;但 Go 会做边界检查,而且数组赋值会复制整个数组。
+
+## 2. 用 range 遍历
+
+```go
+for index, score := range scores {
+    fmt.Println(index, score)
+}
+```
+
+`index` 是索引,`score` 是当前元素值的副本。只需要值时,写 `for _, score := range scores`。
+
+修改 `score` 不会直接修改数组中的整数元素。要修改原元素,应通过索引,例如 `scores[index] = score + 1`。
+
+## 3. 切片:常用的序列类型
+
+```go
+numbers := []int{10, 20, 30}
+numbers = append(numbers, 40, 50)
+```
+
+| 对比 | 数组 | 切片 |
+| --- | --- | --- |
+| 类型示例 | `[3]int` | `[]int` |
+| 长度 | 固定,属于类型的一部分 | 运行时可变化,不属于类型的一部分 |
+| 追加 | 不能直接 append | 使用 append 并接收返回值 |
+| 赋值 | 复制整个数组 | 复制切片描述信息,可能共享底层数组 |
+
+`append` 返回更新后的切片值,要写回变量。底层空间不足时可能分配新的数组,不要假设追加后原来的共享关系始终不变,也不要依赖固定的容量增长倍数。
+
+## 4. 截取与共享
+
+```go
+middle := numbers[1:3]
+```
+
+范围是**左闭右开**:包含索引 1、2,不包含 3;因此长度是 `3-1=2`。
+
+常见写法还有 `numbers[:2]`(前两个元素)、`numbers[2:]`(从索引 2 到末尾)、`numbers[:]`(全部)。
+
+截取得到的子切片不是独立副本,它与原切片共享底层数组。因此本课 `middle[0] = 200` 会修改 `numbers[1]`。
+
+`alias := numbers` 也不是复制全部元素;修改 `alias` 中已有元素,可能影响 `numbers`。
+
+## 5. 独立复制
+
+```go
+backup := make([]int, len(numbers))
+copied := copy(backup, numbers)
+```
+
+`make` 创建目标切片,`copy` 复制元素并返回实际复制数量,即两边长度的较小值。目标长度不够时,不会自动扩容。
+
+本课元素是整数,所以这样能得到不共享元素存储的独立副本;将来元素包含切片或指针时,还需要理解浅拷贝。
+
+## 6. 零值、长度与容量
+
+- `var tasks []string` 的零值为 `nil`,可以直接 append。
+- `len` 是当前可访问的元素数量。
+- `cap` 是从切片起点到底层数组末尾的容量,不是可直接使用索引访问的范围。
+- 即使 `cap` 大于 `len`,也不能直接访问 `slice[len(slice)]`,应先 append 或合法地重新切片。
+- 空的非 nil 切片也存在,例如 `[]int{}`;长度为 0 并不一定表示 nil。
+
+## 编译并运行
+
+在**仓库根目录**执行:
+
+```powershell
+New-Item -ItemType Directory -Force bin/05_arrays_slices | Out-Null
+go build -o ./bin/05_arrays_slices/arrays_slices.exe ./lessons/05_arrays_slices
+.\bin\05_arrays_slices\arrays_slices.exe
+```
+
+修改源码后需要重新编译。快速编译并运行也可以用:
+
+```powershell
+go run ./lessons/05_arrays_slices
+```
+
+或者从根目录进入本课,再执行 `go run .`:
+
+```powershell
+Set-Location lessons/05_arrays_slices
+go run .
+```
+
+## 预期输出
+
+```text
+=== 数组 ===
+成绩:[80 90 75],长度:3
+第一个成绩:80
+修改后的成绩:[80 90 85]
+
+=== 遍历与求和 ===
+第 1 个成绩:80
+第 2 个成绩:90
+第 3 个成绩:85
+总分:255,平均分:85.00
+
+=== 切片与追加 ===
+切片:[10 20 30 40 50],长度:5
+
+=== 截取与共享 ===
+numbers[1:3]:[20 30]
+修改子切片后,原切片:[10 200 30 40 50]
+
+=== 复制切片 ===
+复制了 5 个元素,副本:[999 200 30 40 50]
+原切片保持不变:[10 200 30 40 50]
+
+=== 从空切片开始 ===
+追加前:nil=true,长度=0,容量=0
+任务:复习循环
+任务:学习切片
+
+=== 数组赋值是复制 ===
+数组副本:[100 90 85],原数组:[80 90 85]
+```
+
+## 动手练习
+
+- [ ] 修改三个成绩,重新核对总分和平均分。
+- [ ] 给 `numbers` 再追加两个整数,观察长度变化。
+- [ ] 改为截取 `numbers[:2]`,预测输出后再运行。
+- [ ] 观察修改子切片与修改独立副本,对原切片的影响有何不同。
+- [ ] 为 `tasks` 追加第三个任务。
+- [ ] 在求和循环中结合 `if score >= 90`,统计优秀成绩数量。
+
+特别注意:本例数组非空,可以计算平均值;未来处理空切片时,应先判断长度,避免除以零。
+
+如果另外手敲一份,放进本课的 `practice/` 子目录,用 `go run ./lessons/05_arrays_slices/practice` 运行,不要在同一个包中声明两个 `main` 函数。
+
+下一课建议学 `map`,认识按“键”查找数据的方式。
+
+[上一课:for 循环](../04_loops/README.md) · [学习总览](../../README.md)

+ 62 - 0
lessons/05_arrays_slices/main.go

@@ -0,0 +1,62 @@
+package main
+
+import "fmt"
+
+func main() {
+	// 1. 数组:长度固定,所有元素类型相同,索引从 0 开始。
+	fmt.Println("=== 数组 ===")
+	var scores [3]int = [3]int{80, 90, 75}
+	fmt.Printf("成绩:%v,长度:%d\n", scores, len(scores))
+	fmt.Printf("第一个成绩:%d\n", scores[0])
+	scores[2] = 85
+	fmt.Printf("修改后的成绩:%v\n", scores)
+
+	// 2. range 遍历数组:得到索引和值。
+	fmt.Println("\n=== 遍历与求和 ===")
+	sum := 0
+	for index, score := range scores {
+		fmt.Printf("第 %d 个成绩:%d\n", index+1, score)
+		sum += score
+	}
+	average := float64(sum) / float64(len(scores))
+	fmt.Printf("总分:%d,平均分:%.2f\n", sum, average)
+
+	// 3. 切片:类型写作 []int,不指定数组长度。
+	// append 返回新的切片值,必须接收返回值。
+	fmt.Println("\n=== 切片与追加 ===")
+	numbers := []int{10, 20, 30}
+	numbers = append(numbers, 40, 50)
+	fmt.Printf("切片:%v,长度:%d\n", numbers, len(numbers))
+
+	// 4. 截取切片:左闭右开,包含索引 1,不包含索引 3。
+	// 子切片和原切片共享底层数组,修改元素可能影响彼此。
+	fmt.Println("\n=== 截取与共享 ===")
+	middle := numbers[1:3]
+	fmt.Printf("numbers[1:3]:%v\n", middle)
+	middle[0] = 200
+	fmt.Printf("修改子切片后,原切片:%v\n", numbers)
+
+	// 5. 使用 make 和 copy 创建独立副本。
+	// make([]int, n) 创建长度为 n、元素初值为 0 的切片。
+	fmt.Println("\n=== 复制切片 ===")
+	backup := make([]int, len(numbers))
+	copied := copy(backup, numbers)
+	backup[0] = 999
+	fmt.Printf("复制了 %d 个元素,副本:%v\n", copied, backup)
+	fmt.Printf("原切片保持不变:%v\n", numbers)
+
+	// 6. 切片零值是 nil,长度与容量都是 0,可以直接 append。
+	fmt.Println("\n=== 从空切片开始 ===")
+	var tasks []string
+	fmt.Printf("追加前:nil=%t,长度=%d,容量=%d\n", tasks == nil, len(tasks), cap(tasks))
+	tasks = append(tasks, "复习循环", "学习切片")
+	for _, task := range tasks {
+		fmt.Printf("任务:%s\n", task)
+	}
+
+	// 7. 数组赋值会复制整个数组,与切片赋值的共享行为不同。
+	fmt.Println("\n=== 数组赋值是复制 ===")
+	otherScores := scores
+	otherScores[0] = 100
+	fmt.Printf("数组副本:%v,原数组:%v\n", otherScores, scores)
+}

+ 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["小朱"])
+}

+ 153 - 0
lessons/07_functions/README.md

@@ -0,0 +1,153 @@
+# 第 07 课:函数
+
+## 学习目标
+
+第一课已经见过函数,这一课正式学习怎样拆分逻辑:参数传入数据、return 返回结果、多返回值表达是否有效,以及可变参数。
+
+先手敲 [main.go](./main.go),运行成功后再阅读配套的 [main_test.go](./main_test.go)。不要一开始就同时抄完测试代码。本课共享根目录的 `go.mod`,无需第三方依赖。
+
+## 1. 声明和调用
+
+```go
+func add(a, b int) int {
+    return a + b
+}
+
+result := add(2, 3) // 这一行调用放在 main 等函数内部。
+```
+
+- `func`:声明函数。
+- `add`:函数名。
+- `a, b int`:两个 int 参数,完整写法是 `a int, b int`。
+- 参数列表后面的 `int`:返回值类型。
+- `return`:结束当前函数,并把结果交给调用方;不是结束整个程序。
+
+与 C 相比,Go 的类型写在名称后面,而且支持直接返回多个值。不需要为后面定义的普通函数先写函数原型,也不能直接在另一个函数内部声明具名函数;后续会学匿名函数。
+
+函数定义的前后顺序不决定调用顺序:程序完成初始化后进入 main,按实际调用执行。普通函数不会因为写在文件里就自动运行。
+
+## 2. 参数与值传递
+
+```go
+func increase(value int) int {
+    value++
+    return value
+}
+```
+
+调用 `increase(count)` 时,参数接收的是值的副本;修改内部整数 value,不会改变调用方的 count。想保留新值,可以用 `count = increase(count)`。
+
+Go 的参数都是按值传递。但切片和 map 的值包含对底层数据的引用,复制参数值不等于复制全部数据:函数内修改切片元素或 map 条目,可能影响调用方。不能理解为“传进函数的任何数据都完全独立”。指针和更深入的共享行为后续单独学习。
+
+## 3. 多返回值与提前返回
+
+```go
+func average(scores []int) (float64, bool) {
+    if len(scores) == 0 {
+        return 0, false
+    }
+    // 累加计算……
+    return 85, true // 此处仅示意返回形式,完整计算见 main.go。
+}
+```
+
+调用时按顺序接收:
+
+```go
+avg, ok := average(scores)
+```
+
+本课约定:ok 为 true 才表示平均值有效。空切片提前返回 false;真正的零分则返回 `(0, true)`,避免混淆。
+
+不需要某个返回值,可以使用 `_`,例如 `_, ok = average(nil)`。这里使用 `=`,因为 ok 已在同一作用域声明。
+
+该函数只检查是否为空,不验证成绩是否位于 0~100,也暂不处理极大整数累加溢出。后续可在学习 error 后扩展输入校验。
+
+## 4. 可变参数
+
+```go
+func sum(values ...int) int {
+    total := 0
+    for _, value := range values {
+        total += value
+    }
+    return total
+}
+```
+
+- 调用 `sum(1, 2, 3)` 可以传多个整数,`sum()` 也合法。
+- 在函数内部,values 作为 `[]int` 使用。
+- 已有切片时写 `sum(numbers...)`,将切片展开为参数。
+- 如果有其他参数,可变参数必须放在最后。
+
+先掌握常规函数即可,本课暂不引入递归、闭包、泛型和 defer。
+
+## 编译并运行
+
+在**仓库根目录**执行:
+
+```powershell
+New-Item -ItemType Directory -Force bin/07_functions | Out-Null
+go build -o ./bin/07_functions/functions.exe ./lessons/07_functions
+.\bin\07_functions\functions.exe
+```
+
+修改源码后需要重新编译。快速编译并运行:
+
+```powershell
+go run ./lessons/07_functions
+```
+
+或者从根目录进入本课:
+
+```powershell
+Set-Location lessons/07_functions
+go run .
+```
+
+## 预期输出
+
+```text
+=== 无参数函数 ===
+欢迎学习 Go 函数
+
+=== 参数与返回值 ===
+2 + 3 = 5
+
+=== 值传递 ===
+原变量:10,函数返回的新值:11
+
+=== 多返回值与提前返回 ===
+平均分:85.00
+没有成绩,无法计算平均分
+
+=== 可变参数 ===
+直接传入多个整数:6
+展开切片作为参数:60
+不传参数:0
+```
+
+## 测试
+
+在仓库根目录执行:
+
+```powershell
+go test -v ./lessons/07_functions
+```
+
+测试覆盖加法、整数值传递、正常平均值、小数平均值、零分、空切片和可变参数。这里的小数用例是可以精确表示的 80.5,因此直接比较;一般浮点计算测试可能需要容差判断。
+
+## 动手练习
+
+- [ ] 把 add 的输入改成负数,观察结果。
+- [ ] 用 `count = increase(count)` 更新调用方变量,比较前后区别。
+- [ ] 修改成绩切片,核对平均分;再改为空切片,观察提前返回。
+- [ ] 新写 `max(a, b int) int`,返回较大的值。
+- [ ] 为 max 添加相等、正数、负数的测试用例。
+- [ ] 使用 sum 计算一组新整数的总和。
+
+若另行手敲,放到本课的 `practice/` 子目录,用 `go run ./lessons/07_functions/practice` 运行,不要在同一个包中重复声明 main 或其他同名函数。
+
+下一课建议学习结构体,把姓名、年龄、成绩等不同类型的数据组合成一个对象。
+
+[上一课:map 键值对](../06_maps/README.md) · [学习总览](../../README.md)

+ 75 - 0
lessons/07_functions/main.go

@@ -0,0 +1,75 @@
+package main
+
+import "fmt"
+
+// 1. 无参数、无返回值。main 可以调用它,函数定义不必放在 main 前面。
+func greet() {
+	fmt.Println("欢迎学习 Go 函数")
+}
+
+// 2. 参数类型写在名称后面,返回值类型写在参数列表后面。
+// 相同类型的相邻参数,可以简写成 a, b int。
+func add(a, b int) int {
+	return a + b
+}
+
+// 3. int 参数按值传递:修改形参不会修改调用方的整数变量。
+func increase(value int) int {
+	value++
+	return value
+}
+
+// 4. 多返回值:用 bool 表示本次计算是否有效。
+// 本课先用 bool,后续再学习 Go 中常见的 (结果, error) 形式。
+func average(scores []int) (float64, bool) {
+	if len(scores) == 0 {
+		return 0, false // 提前返回,避免空切片导致除以零。
+	}
+
+	total := 0
+	for _, score := range scores {
+		total += score
+	}
+	return float64(total) / float64(len(scores)), true
+}
+
+// 5. 可变参数:可接收零个或多个 int,函数内部 values 是 []int。
+func sum(values ...int) int {
+	total := 0
+	for _, value := range values {
+		total += value
+	}
+	return total
+}
+
+func main() {
+	fmt.Println("=== 无参数函数 ===")
+	greet()
+
+	fmt.Println("\n=== 参数与返回值 ===")
+	result := add(2, 3)
+	fmt.Printf("2 + 3 = %d\n", result)
+
+	fmt.Println("\n=== 值传递 ===")
+	count := 10
+	next := increase(count)
+	fmt.Printf("原变量:%d,函数返回的新值:%d\n", count, next)
+
+	fmt.Println("\n=== 多返回值与提前返回 ===")
+	scores := []int{80, 90, 85}
+	avg, ok := average(scores)
+	if ok {
+		fmt.Printf("平均分:%.2f\n", avg)
+	}
+
+	_, ok = average(nil) // 不需要结果时,用 _ 忽略它。
+	if !ok {
+		fmt.Println("没有成绩,无法计算平均分")
+	}
+
+	fmt.Println("\n=== 可变参数 ===")
+	fmt.Printf("直接传入多个整数:%d\n", sum(1, 2, 3))
+	numbers := []int{10, 20, 30}
+	fmt.Printf("展开切片作为参数:%d\n", sum(numbers...))
+	fmt.Printf("不传参数:%d\n", sum())
+}

+ 55 - 0
lessons/07_functions/main_test.go

@@ -0,0 +1,55 @@
+package main
+
+import "testing"
+
+func TestAdd(t *testing.T) {
+	if got := add(2, 3); got != 5 {
+		t.Fatalf("add(2, 3) = %d, want 5", got)
+	}
+}
+
+func TestIncrease(t *testing.T) {
+	value := 10
+	if got := increase(value); got != 11 {
+		t.Fatalf("increase(10) = %d, want 11", got)
+	}
+	if value != 10 {
+		t.Fatalf("caller value = %d, want 10", value)
+	}
+}
+
+func TestAverage(t *testing.T) {
+	tests := []struct {
+		name   string
+		scores []int
+		want   float64
+		ok     bool
+	}{
+		{name: "normal", scores: []int{80, 90, 85}, want: 85, ok: true},
+		{name: "fraction", scores: []int{80, 81}, want: 80.5, ok: true},
+		{name: "zero", scores: []int{0}, want: 0, ok: true},
+		{name: "empty", scores: []int{}, want: 0, ok: false},
+		{name: "nil", scores: nil, want: 0, ok: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, ok := average(tt.scores)
+			if got != tt.want || ok != tt.ok {
+				t.Fatalf("average(%v) = (%v, %t), want (%v, %t)", tt.scores, got, ok, tt.want, tt.ok)
+			}
+		})
+	}
+}
+
+func TestSum(t *testing.T) {
+	if got := sum(1, 2, 3); got != 6 {
+		t.Fatalf("sum(1, 2, 3) = %d, want 6", got)
+	}
+	if got := sum(); got != 0 {
+		t.Fatalf("sum() = %d, want 0", got)
+	}
+	values := []int{10, 20, 30}
+	if got := sum(values...); got != 60 {
+		t.Fatalf("sum(%v...) = %d, want 60", values, got)
+	}
+}

+ 214 - 0
lessons/08_structs/README.md

@@ -0,0 +1,214 @@
+# 第 08 课:结构体 struct
+
+## 学习目标
+
+把姓名、年龄、成绩等不同类型的数据组合成一条记录,理解上一课测试中的 `[]struct`。
+
+按 [main.go](./main.go) 的 1~8 节逐段手敲。本课只学习普通结构体,不引入指针、方法、接口或匿名函数,也不新增复杂测试语法。共享根目录的 `go.mod`,无需第三方依赖。
+
+## 1. 定义类型,不是创建变量
+
+```go
+type Student struct {
+    Name  string
+    Age   int
+    Score float64
+}
+```
+
+逐项理解:
+
+- `type`:定义一个类型。
+- `Student`:自己取的类型名,不是变量名。
+- `struct`:这个类型是结构体。
+- `Name`、`Age`、`Score`:字段名,后面是字段类型。
+
+定义类型只是规定“数据长什么样”,还没有创建某个具体学生。
+
+与 C 类似,结构体用来组合不同类型的数据。Go 定义类型后,可直接写 `Student`,不必再加 `struct Student`,定义末尾也不需要 C 风格的分号。
+
+## 2. 创建变量与访问字段
+
+```go
+student := Student{
+    Name:  "小朱",
+    Age:   20,
+    Score: 90.5,
+}
+
+fmt.Println(student.Name)
+student.Age = 21
+```
+
+- `Student{...}`:创建一个 Student 类型的值,不是调用函数。
+- `Name: "小朱"`:给 Name 字段赋初始值。
+- `student.Name`:通过点号访问字段。
+- 多行初始化时,最后一项后面也需要逗号。
+
+本课统一使用带字段名的初始化,避免把值的顺序写错。未列出的字段会使用零值;例如 `Student{Name: "小朱"}` 的年龄和成绩都是 0。
+
+结构体字段名大小写决定跨包可见性:大写开头可从其他包访问,小写开头仅包内可访问。当前都在同一个包,先记住这条规则即可。
+
+## 3. 零值与复制
+
+```go
+var empty Student
+```
+
+字段自动获得各自的零值:Name 是 `""`,Age 是 `0`,Score 是 `0`。
+
+```go
+copied := student
+copied.Name = "小李"
+```
+
+结构体赋值会复制结构体的值。本课字段都是 string、int、float64,修改 copied 的字段不会改变 student。
+
+但这不是所有情况下的深拷贝:如果将来结构体里包含切片、map 或指针,复制后仍可能共享它们引用的数据。不要理解为“任何结构体赋值都会递归复制全部底层数据”。
+
+## 4. 作为函数参数
+
+```go
+func printStudent(student Student) {
+    fmt.Println(student.Name)
+}
+```
+
+结构体也可以作为普通函数参数:变量名是 student,参数类型是 Student。按上一课的值传递规则,参数接收结构体的副本。本课函数只负责读取和打印。
+
+## 5. 结构体切片
+
+```go
+students := []Student{
+    {Name: "小朱", Age: 21, Score: 90.5},
+    {Name: "小李", Age: 19, Score: 80},
+}
+```
+
+`[]Student` 与 `[]int` 一样都是切片,只是元素类型变成了 Student。内层省略 Student,是因为外层已经说明元素类型;每一对内层大括号代表一条记录。
+
+访问第一个学生的姓名:
+
+```go
+fmt.Println(students[0].Name)
+```
+
+遍历读取:
+
+```go
+for _, item := range students {
+    fmt.Println(item.Name)
+}
+```
+
+item 是当前元素的副本,给 `item.Age` 赋值不会直接更新原切片中的这个整数。修改原元素时使用索引:
+
+```go
+for index := range students {
+    students[index].Age++
+}
+```
+
+这里 range 只有一个接收变量,取得的是索引。代码中的成绩切片保证非空;如果改为可能为空的数据,计算平均值之前应先检查长度。
+
+## 6. 读懂上一课的 []struct
+
+```go
+tests := []struct {
+    name string
+    a    int
+    b    int
+    want int
+}{
+    {name: "正数相加", a: 2, b: 3, want: 5},
+    {name: "正负相加", a: -2, b: 3, want: 1},
+}
+```
+
+拆成三层:
+
+1. `struct { ... }`:定义一个没有单独类型名的结构体类型。
+2. `[]struct { ... }`:元素是这种结构体的切片。
+3. 后面的 `{ ... }`:填入多条记录,每条记录包含名称、输入和期望结果。
+
+它不是 map,`name:` 是指定结构体字段。name、want、tests、tt 都是普通名称,不是 Go 关键字。
+
+也可以先定义命名类型,再写同样的数据:
+
+```go
+type AddCase struct {
+    name string
+    a    int
+    b    int
+    want int
+}
+
+tests := []AddCase{
+    {name: "正数相加", a: 2, b: 3, want: 5},
+}
+```
+
+本课只循环计算并打印实际值与期望值,不用 t.Run。打印“通过=false”也不会自动让 `go test` 失败;正式测试还要使用 testing 包报告失败。匿名函数和 t.Run 留到后续专题。
+
+## 编译并运行
+
+在**仓库根目录**执行:
+
+```powershell
+New-Item -ItemType Directory -Force bin/08_structs | Out-Null
+go build -o ./bin/08_structs/structs.exe ./lessons/08_structs
+.\bin\08_structs\structs.exe
+```
+
+修改源码后需要重新编译。快速编译并运行:
+
+```powershell
+go run ./lessons/08_structs
+```
+
+或者从根目录进入本课:
+
+```powershell
+Set-Location lessons/08_structs
+go run .
+```
+
+## 预期输出
+
+```text
+=== 创建与访问字段 ===
+姓名:小朱,年龄:20,成绩:90.5
+修改后的年龄:21
+
+=== 结构体零值 ===
+姓名:"",年龄:0,成绩:0.0
+
+=== 结构体赋值 ===
+原姓名:小朱,副本姓名:小李
+
+=== 结构体切片 ===
+姓名:小朱,年龄:21,成绩:90.5
+姓名:小李,年龄:19,成绩:80.0
+姓名:小陈,年龄:22,成绩:75.5
+总分:246.0,平均分:82.00
+所有学生年龄加一后,第一位年龄:22
+
+=== 理解测试数据表 ===
+正数相加:实际=5,期望=5,通过=true
+正负相加:实际=1,期望=1,通过=true
+```
+
+## 动手练习
+
+- [ ] 给 Student 增加 `City string` 字段,并打印城市。
+- [ ] 只初始化 Name,观察其他字段的零值。
+- [ ] 向 students 追加一名学生,重新计算平均分。
+- [ ] 结合 if,打印成绩大于等于 90 的学生姓名。
+- [ ] 将匿名结构体测试表改成命名的 AddCase 类型,确认结果不变。
+- [ ] 故意把一条 want 改错,观察实际值和期望值的区别,然后恢复。
+
+若另外手敲一份,放进本课 `practice/` 子目录,用 `go run ./lessons/08_structs/practice` 运行,不要在同一个包里重复定义 Student、main 或其他同名函数。
+
+下一课学习指针,解释 `&`、`*` 和怎样通过函数修改调用方的数据。
+
+[上一课:函数](../07_functions/README.md) · [学习总览](../../README.md)

+ 78 - 0
lessons/08_structs/main.go

@@ -0,0 +1,78 @@
+package main
+
+import "fmt"
+
+// 1. 定义结构体类型:把不同类型的相关数据组合在一起。
+// type 表示定义类型,Student 是类型名,Name / Age / Score 是字段。
+type Student struct {
+	Name  string
+	Age   int
+	Score float64
+}
+
+// 2. 结构体也可以作为普通函数的参数。
+// 这里传入的是 Student 值的副本;本函数只读取它。
+func printStudent(student Student) {
+	fmt.Printf("姓名:%s,年龄:%d,成绩:%.1f\n", student.Name, student.Age, student.Score)
+}
+
+func main() {
+	// 3. 使用字段名初始化。多行写法的最后一个字段也需要逗号。
+	fmt.Println("=== 创建与访问字段 ===")
+	student := Student{
+		Name:  "小朱",
+		Age:   20,
+		Score: 90.5,
+	}
+	printStudent(student)
+	student.Age = 21
+	fmt.Printf("修改后的年龄:%d\n", student.Age)
+
+	// 4. 没有显式赋值的字段会得到各自类型的零值。
+	fmt.Println("\n=== 结构体零值 ===")
+	var empty Student
+	fmt.Printf("姓名:%q,年龄:%d,成绩:%.1f\n", empty.Name, empty.Age, empty.Score)
+
+	// 5. 本课结构体只包含 string / int / float64,赋值后修改副本不影响原值。
+	fmt.Println("\n=== 结构体赋值 ===")
+	copied := student
+	copied.Name = "小李"
+	fmt.Printf("原姓名:%s,副本姓名:%s\n", student.Name, copied.Name)
+
+	// 6. []Student 是结构体切片,每个元素保存一名学生的信息。
+	fmt.Println("\n=== 结构体切片 ===")
+	students := []Student{
+		{Name: "小朱", Age: 21, Score: 90.5},
+		{Name: "小李", Age: 19, Score: 80},
+		{Name: "小陈", Age: 22, Score: 75.5},
+	}
+	total := 0.0
+	for _, item := range students {
+		printStudent(item)
+		total += item.Score
+	}
+	fmt.Printf("总分:%.1f,平均分:%.2f\n", total, total/float64(len(students)))
+
+	// 7. range 得到的 item 是元素副本。要修改原元素,使用索引。
+	for index := range students {
+		students[index].Age++
+	}
+	fmt.Printf("所有学生年龄加一后,第一位年龄:%d\n", students[0].Age)
+
+	// 8. 不单独起类型名的结构体切片:对应上一课测试中的 []struct。
+	// 本课先把它当作“由多条记录组成的表”,只用普通循环读取。
+	fmt.Println("\n=== 理解测试数据表 ===")
+	tests := []struct {
+		name string
+		a    int
+		b    int
+		want int
+	}{
+		{name: "正数相加", a: 2, b: 3, want: 5},
+		{name: "正负相加", a: -2, b: 3, want: 1},
+	}
+	for _, tt := range tests {
+		got := tt.a + tt.b
+		fmt.Printf("%s:实际=%d,期望=%d,通过=%t\n", tt.name, got, tt.want, got == tt.want)
+	}
+}

+ 183 - 0
lessons/09_pointers/README.md

@@ -0,0 +1,183 @@
+# 第 09 课:指针
+
+## 学习目标
+
+学会区分变量的值、变量的地址和指针变量,理解怎样通过函数修改调用方的数据,以及测试参数 `t *testing.T` 中星号的含义。
+
+按 [main.go](./main.go) 的 1~7 节逐段手敲。本课只使用普通指针和结构体,不涉及二级指针、unsafe 或复杂内存管理。共享根目录的 `go.mod`,无需第三方依赖。
+
+## 1. 三个写法,分别看待
+
+```go
+number := 10
+var pointer *int = &number
+fmt.Println(*pointer)
+```
+
+| 写法 | 含义 |
+| --- | --- |
+| `number` | 一个整数变量,当前值为 10 |
+| `&number` | 取得 number 的地址 |
+| `*int` | 类型:指向 int 的指针 |
+| `pointer` | 保存地址的指针变量 |
+| `*pointer` | 表达式:访问 pointer 指向的变量,称为解引用 |
+
+星号的位置不同,含义不同:声明中的 `*int` 是类型,表达式中的 `*pointer` 是访问目标。`*pointer = 20` 修改目标整数;`pointer = &other` 则让指针改为指向另一个整数。
+
+可以用 `fmt.Printf("地址:%p\n", pointer)` 观察地址。具体地址不固定,不要背诵某次输出,也不要依赖两次运行地址相同。示例用地址比较保证默认输出稳定。
+
+## 2. 函数参数仍然按值传递
+
+普通参数:
+
+```go
+func changeValue(value int) {
+    value = 100
+}
+```
+
+调用 `changeValue(number)` 只修改整数副本,不影响 number。
+
+指针参数:
+
+```go
+func changeByPointer(value *int) bool {
+    if value == nil {
+        return false
+    }
+    *value = 100
+    return true
+}
+```
+
+调用 `changeByPointer(&number)` 时,把地址值复制给参数 value。value 和调用方都能找到同一个整数,因此通过 `*value` 可以修改 number。
+
+**Go 仍然是按值传递,不是出现指针后就变成另一种参数传递规则。** 复制的是地址值,而不是目标整数。
+
+另外,在函数内写 `value = &local` 只改变参数指针的副本,不会改变调用方指针本身。示例中的 redirect 专门演示这个区别。本课不需要掌握怎样修改调用方指针的指向。
+
+## 3. nil 指针
+
+```go
+var empty *int
+```
+
+指针零值是 nil,表示没有指向有效变量。可以判断 `empty == nil`,但不能执行 `*empty`,否则会发生运行时 panic。
+
+指针参数是否允许 nil,取决于函数约定。本课函数主动检查,遇到 nil 返回 false;不能因此假设所有库函数都接受 nil。
+
+`var empty *int` 不会自动创建一个目标整数。与之不同:
+
+```go
+created := new(int)
+```
+
+会创建一个零值 int 并返回它的指针,`created != nil`,`*created == 0`。通常 `number := 0; pointer := &number` 也能满足类似需求,不必强制使用 new。
+
+## 4. 结构体指针
+
+```go
+func birthday(student *Student) bool {
+    if student == nil {
+        return false
+    }
+    student.Age++
+    return true
+}
+```
+
+调用 `birthday(&student)`,修改的是原学生的年龄。
+
+Go 允许直接写 `student.Age`,这里等价于 `(*student).Age`。Go 没有 C 中的 `->` 运算符;nil 结构体指针也不能直接访问字段。
+
+## 5. 再看测试参数
+
+```go
+func TestAdd(t *testing.T) {
+    // t.Fatalf(...) 等操作由 testing 包提供。
+}
+```
+
+现在可以拆开理解:
+
+- t:参数变量名。
+- testing.T:testing 包中提供的测试管理类型。
+- `*testing.T`:指向这个类型的指针。
+- 测试框架调用函数时传入 t,用户不需要自己创建或调用 TestAdd。
+
+`t.Fatalf` 不是结构体里的普通数据字段,而是 testing.T 提供的方法,下一课学习“方法”时再深入理解。本课不需要手动创建 testing.T。
+
+## 6. 与 C 相比
+
+- 都用 `&` 取地址,用 `*` 声明指针类型或解引用。
+- Go 的指针不支持普通 C 风格的 `p++`、`p + 1` 指针算术。
+- 普通 Go 代码不需要手动 malloc / free,内存由运行时垃圾回收管理;文件等系统资源仍需要适当关闭。
+- 可以返回局部变量的地址,Go 会保证仍被使用的数据保持有效;不要直接套用 C 的栈上悬空指针规则。具体分配在栈还是堆由编译器分析,不由 `&` 或 new 单独决定。
+- 指针能修改共享变量,但不自动提供并发安全,后续并发课程再讨论同步。
+
+## 编译并运行
+
+在**仓库根目录**执行:
+
+```powershell
+New-Item -ItemType Directory -Force bin/09_pointers | Out-Null
+go build -o ./bin/09_pointers/pointers.exe ./lessons/09_pointers
+.\bin\09_pointers\pointers.exe
+```
+
+修改源码后需要重新编译。快速编译并运行:
+
+```powershell
+go run ./lessons/09_pointers
+```
+
+或者从根目录进入本课:
+
+```powershell
+Set-Location lessons/09_pointers
+go run .
+```
+
+## 预期输出
+
+```text
+=== 取地址与解引用 ===
+number 的值:10
+指针类型:*int,是否指向 number:true
+通过 *pointer 读取:10
+通过指针修改后,number:20
+
+=== 函数修改原变量 ===
+普通参数调用后:20
+指针参数调用后:100,成功:true
+
+=== 指针复制与重新指向 ===
+通过 alias 修改后,number:30
+函数内重新指向后,原指针仍指向 number:true
+
+=== nil 指针 ===
+空指针为 nil:true
+传入 nil,修改成功:false
+
+=== 结构体指针 ===
+小朱 过生日后,年龄:21
+
+=== new 创建指针 ===
+指针为 nil:false,初始值:0
+修改后的值:42
+```
+
+## 动手练习
+
+- [ ] 用 `%p` 打印 `&number` 和 pointer,确认它们相等。
+- [ ] 创建另一个整数 other,让 pointer 指向它,观察 `*pointer = 50` 修改了谁。
+- [ ] 给 birthday 传入 nil,检查返回结果。
+- [ ] 新写 `reset(value *int) bool`,检查 nil 后把目标整数设为 0。
+- [ ] 对比 `var p *int` 与 `p := new(int)` 的 nil 状态和是否能解引用。
+- [ ] 回看第七课的 `t *testing.T`,区分参数名、包名和类型。
+
+如果试验 nil 解引用,一次只保留这一处故意错误,观察 panic 后恢复注释。若另行手敲,放到本课的 `practice/` 子目录,用 `go run ./lessons/09_pointers/practice` 运行,不要在同一个包中重复定义 Student 或 main。
+
+下一课学习方法:理解接收者、值接收者与指针接收者,让结构体拥有自己的操作。
+
+[上一课:结构体](../08_structs/README.md) · [学习总览](../../README.md)

+ 87 - 0
lessons/09_pointers/main.go

@@ -0,0 +1,87 @@
+package main
+
+import "fmt"
+
+type Student struct {
+	Name string
+	Age  int
+}
+
+// 传入整数副本,只修改函数内部的 value。
+func changeValue(value int) {
+	value = 100
+}
+
+// 指针参数本身也按值传递,但它指向调用方的整数。
+// 返回 bool 表示是否成功,避免解引用 nil 指针。
+func changeByPointer(value *int) bool {
+	if value == nil {
+		return false
+	}
+	*value = 100
+	return true
+}
+
+// 给参数指针重新赋值,不会改变调用方指针本身。
+func redirect(value *int) {
+	local := 999
+	value = &local
+}
+
+// 结构体指针可以直接用点号访问字段,无需 C 风格的 ->。
+func birthday(student *Student) bool {
+	if student == nil {
+		return false
+	}
+	student.Age++ // 等价于 (*student).Age++。
+	return true
+}
+
+func main() {
+	// 1. & 取得变量地址;*int 表示“指向 int 的指针类型”。
+	fmt.Println("=== 取地址与解引用 ===")
+	number := 10
+	var pointer *int = &number
+	fmt.Printf("number 的值:%d\n", number)
+	fmt.Printf("指针类型:%T,是否指向 number:%t\n", pointer, pointer == &number)
+	fmt.Printf("通过 *pointer 读取:%d\n", *pointer)
+
+	// 2. *pointer 在表达式中表示访问所指向的变量。
+	*pointer = 20
+	fmt.Printf("通过指针修改后,number:%d\n", number)
+
+	// 3. 对比普通参数与指针参数。
+	fmt.Println("\n=== 函数修改原变量 ===")
+	changeValue(number)
+	fmt.Printf("普通参数调用后:%d\n", number)
+	ok := changeByPointer(&number)
+	fmt.Printf("指针参数调用后:%d,成功:%t\n", number, ok)
+
+	// 4. 复制指针仍指向同一变量;改指向和改内容是两回事。
+	fmt.Println("\n=== 指针复制与重新指向 ===")
+	alias := pointer
+	*alias = 30
+	fmt.Printf("通过 alias 修改后,number:%d\n", number)
+	redirect(pointer)
+	fmt.Printf("函数内重新指向后,原指针仍指向 number:%t\n", pointer == &number)
+
+	// 5. 指针零值是 nil,没有指向有效变量时不能解引用。
+	fmt.Println("\n=== nil 指针 ===")
+	var empty *int
+	fmt.Printf("空指针为 nil:%t\n", empty == nil)
+	fmt.Printf("传入 nil,修改成功:%t\n", changeByPointer(empty))
+	// fmt.Println(*empty) // 取消注释会导致运行时 panic。
+
+	// 6. 结合结构体:通过指针修改原学生的年龄。
+	fmt.Println("\n=== 结构体指针 ===")
+	student := Student{Name: "小朱", Age: 20}
+	birthday(&student)
+	fmt.Printf("%s 过生日后,年龄:%d\n", student.Name, student.Age)
+
+	// 7. new(int) 返回指向一个零值 int 的指针,不是 nil。
+	fmt.Println("\n=== new 创建指针 ===")
+	created := new(int)
+	fmt.Printf("指针为 nil:%t,初始值:%d\n", created == nil, *created)
+	*created = 42
+	fmt.Printf("修改后的值:%d\n", *created)
+}