Explorar el Código

feat: 新增第八课结构体例程与逐步学习说明

zhuts hace 3 días
padre
commit
5a0610ebbf
Se han modificado 3 ficheros con 300 adiciones y 4 borrados
  1. 8 4
      README.md
  2. 214 0
      lessons/08_structs/README.md
  3. 78 0
      lessons/08_structs/main.go

+ 8 - 4
README.md

@@ -30,10 +30,13 @@
     ├── 06_maps/            # 第 06 课:map 键值对
     │   ├── README.md       # 增删改查、遍历、nil 与共享
     │   └── main.go         # 按姓名查询成绩的参考代码
-    └── 07_functions/       # 第 07 课:函数
-        ├── README.md       # 参数、返回值、值传递和练习
-        ├── main.go         # 普通函数、多返回值、可变参数
-        └── main_test.go    # 本课函数测试
+    ├── 07_functions/       # 第 07 课:函数
+    │   ├── README.md       # 参数、返回值、值传递和练习
+    │   ├── main.go         # 普通函数、多返回值、可变参数
+    │   └── main_test.go    # 本课函数测试
+    └── 08_structs/         # 第 08 课:结构体
+        ├── README.md       # 定义、字段、复制和结构体切片
+        └── main.go         # 学生记录与简化测试数据表
 ```
 
 根目录不放例程代码。各课共享根目录的 `go.mod`,不需要逐课初始化模块,也不需要 `go.work`。当前模块路径 `example.com/go-study` 是学习用标识,不必与 Git 地址相同。
@@ -49,6 +52,7 @@
 | 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) | 定义、字段访问、零值、复制、结构体切片 | 已建立 |
 
 后续按编号逐步新增独立目录。不一次性铺开后续课程,也不把新例程写进旧例程的入口文件。
 

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