|
|
@@ -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)
|