zhuts 5a0610ebbf feat: 新增第八课结构体例程与逐步学习说明 3 дней назад
..
README.md 5a0610ebbf feat: 新增第八课结构体例程与逐步学习说明 3 дней назад
main.go 5a0610ebbf feat: 新增第八课结构体例程与逐步学习说明 3 дней назад

README.md

第 08 课:结构体 struct

学习目标

把姓名、年龄、成绩等不同类型的数据组合成一条记录,理解上一课测试中的 []struct。

按 main.go 的 1~8 节逐段手敲。本课只学习普通结构体,不引入指针、方法、接口或匿名函数,也不新增复杂测试语法。共享根目录的 go.mod,无需第三方依赖。

1. 定义类型,不是创建变量

type Student struct {
    Name  string
    Age   int
    Score float64
}

逐项理解:

  • type:定义一个类型。
  • Student:自己取的类型名,不是变量名。
  • struct:这个类型是结构体。
  • Name、Age、Score:字段名,后面是字段类型。

定义类型只是规定“数据长什么样”,还没有创建某个具体学生。

与 C 类似,结构体用来组合不同类型的数据。Go 定义类型后,可直接写 Student,不必再加 struct Student,定义末尾也不需要 C 风格的分号。

2. 创建变量与访问字段

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. 零值与复制

var empty Student

字段自动获得各自的零值:Name 是 "",Age 是 0,Score 是 0。

copied := student
copied.Name = "小李"

结构体赋值会复制结构体的值。本课字段都是 string、int、float64,修改 copied 的字段不会改变 student。

但这不是所有情况下的深拷贝:如果将来结构体里包含切片、map 或指针,复制后仍可能共享它们引用的数据。不要理解为“任何结构体赋值都会递归复制全部底层数据”。

4. 作为函数参数

func printStudent(student Student) {
    fmt.Println(student.Name)
}

结构体也可以作为普通函数参数:变量名是 student,参数类型是 Student。按上一课的值传递规则,参数接收结构体的副本。本课函数只负责读取和打印。

5. 结构体切片

students := []Student{
    {Name: "小朱", Age: 21, Score: 90.5},
    {Name: "小李", Age: 19, Score: 80},
}

[]Student 与 []int 一样都是切片,只是元素类型变成了 Student。内层省略 Student,是因为外层已经说明元素类型;每一对内层大括号代表一条记录。

访问第一个学生的姓名:

fmt.Println(students[0].Name)

遍历读取:

for _, item := range students {
    fmt.Println(item.Name)
}

item 是当前元素的副本,给 item.Age 赋值不会直接更新原切片中的这个整数。修改原元素时使用索引:

for index := range students {
    students[index].Age++
}

这里 range 只有一个接收变量,取得的是索引。代码中的成绩切片保证非空;如果改为可能为空的数据,计算平均值之前应先检查长度。

6. 读懂上一课的 []struct

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 关键字。

也可以先定义命名类型,再写同样的数据:

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 留到后续专题。

编译并运行

在仓库根目录执行:

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

修改源码后需要重新编译。快速编译并运行:

go run ./lessons/08_structs

或者从根目录进入本课:

Set-Location lessons/08_structs
go run .

预期输出

=== 创建与访问字段 ===
姓名:小朱,年龄: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 或其他同名函数。

下一课学习指针,解释 &、* 和怎样通过函数修改调用方的数据。

上一课:函数 · 学习总览