Просмотр исходного кода

feat: 新增第五课数组与切片例程及学习说明

zhuts 4 дней назад
Родитель
Сommit
9f7453cbb6
3 измененных файлов с 226 добавлено и 3 удалено
  1. 7 3
      README.md
  2. 157 0
      lessons/05_arrays_slices/README.md
  3. 62 0
      lessons/05_arrays_slices/main.go

+ 7 - 3
README.md

@@ -21,9 +21,12 @@
     ├── 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         # 带逐节注释的参考代码
 ```
 
 根目录不放例程代码。各课共享根目录的 `go.mod`,不需要逐课初始化模块,也不需要 `go.work`。当前模块路径 `example.com/go-study` 是学习用标识,不必与 Git 地址相同。
@@ -36,6 +39,7 @@
 | 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 | 已建立 |
 
 后续按编号逐步新增独立目录。不一次性铺开后续课程,也不把新例程写进旧例程的入口文件。
 

+ 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)
+}