Go 从 0 到精通 · 第 12 课:结构体 struct
Go 从 0 到精通 · 第 12 课:结构体 struct
学习定位:这是整套 Go 教程的第 12 课。
前置要求:已经完成第 11 课,理解字符串的底层表示、byte与rune的区别、字符串不可变性。
本课目标:掌握结构体的定义、初始化、字段访问,理解结构体嵌套与组合,能用结构体建模现实中的业务对象。
1. 本课你要解决的核心问题
到目前为止,你用的都是 Go 内置的类型:int、string、[]int、map[string]int。
但现实中一个"学生"有姓名、年龄、成绩——多个不同类型的数据需要组合在一起。你不可能用一个 int 或一个 string 来描述一个完整的学生。
结构体就是让你把多个相关的字段打包成一个自定义类型。
你需要搞明白以下问题:
- 结构体怎么定义、怎么初始化
- 怎么访问和修改字段
- 结构体是值类型还是引用类型
- 什么是结构体嵌套
- 结构体有哪些常见坑
学完这一课,你就能用结构体来表达现实中的任何业务对象了。
2. 结构体的定义
2.1 基本语法
1 | type 类型名 struct { |
1 | package main |
输出:
1 | {小明 20 92.5} |
type Student struct { ... }定义了一个名为Student的结构体类型- 它有三个字段:
Name、Age、Score s.Name访问s的Name字段
2.2 命名规范
- 类型名用大写驼峰(
PascalCase):Student、BookInfo - 字段名用大写驼峰(首字母大写表示导出,可被其他包访问)
3. 结构体的初始化
3.1 声明后逐个赋值
1 | var s Student |
未赋值的字段保持零值。
3.2 字面量初始化(按顺序)
1 | s := Student{"小明", 20, 92.5} |
按字段定义的顺序传值。不推荐——字段顺序变化时容易出错。
3.3 字面量初始化(按字段名,推荐)
1 | s := Student{ |
按字段名赋值,不依赖顺序。推荐这种写法。
未指定的字段保持零值:
1 | s := Student{Name: "小明"} |
3.4 new 函数(返回指针)
1 | p := new(Student) |
new(Student) 返回 *Student(指向 Student 的指针),所有字段是零值。
p.Name 是语法糖,等价于 (*p).Name——Go 自动解引用。
3.5 取结构体地址
1 | s := Student{Name: "小明", Age: 20} |
4. 访问和修改字段
4.1 用点号访问
1 | s := Student{Name: "小明", Age: 20, Score: 92.5} |
4.2 通过指针访问
1 | p := &Student{Name: "小明", Age: 20} |
Go 的语法糖让你不需要写 (*p).Name,直接用 p.Name 就行。
5. 结构体是值类型
5.1 赋值会复制
1 | s1 := Student{Name: "小明", Age: 20} |
结构体赋值是完整复制。修改 s2 不影响 s1。
5.2 传参也是复制
1 | package main |
输出:
1 | 函数内: 21 |
birthday 拿到的是 s 的副本,修改不影响外部。
5.3 传指针可以修改外部
1 | func birthday(s *Student) { |
这也是实践中最常见的做法:当函数需要修改结构体时,传指针。
6. 结构体嵌套
6.1 基本用法
结构体的字段可以是另一个结构体:
1 | package main |
通过 s.Address.City 访问嵌套结构体的字段。
6.2 匿名嵌套(嵌入)
如果嵌套时省略字段名,就是"嵌入"(embedding):
1 | type Address struct { |
嵌入后,Address 的字段被"提升"到了 Student 的层级,可以直接通过 s.City 访问。
这是 Go 实现"组合"的核心机制之一,第 15 课《组合优于继承》会深入讲解。
6.3 嵌入与命名嵌套的选择
- 命名嵌套:
Address Address,需要通过s.Address.City访问 - 匿名嵌入:
Address,可以通过s.City直接访问
当嵌套结构体的字段名和外层冲突时,必须用完整路径:
1 | type Student struct { |
如果 Student 也加了个 City 字段,就需要用 s.Address.City 来区分。
7. 结构体的比较
7.1 可比较的情况
如果结构体的所有字段都是可比较类型,结构体本身也可以用 == 和 != 比较:
1 | type Point struct { |
7.2 不可比较的情况
如果字段包含切片、map、函数等不可比较类型,结构体就不能用 ==:
1 | type Data struct { |
需要逐个字段比较,或自己写 Equal 方法。
8. 结构体的零值
每个字段按自身类型的零值初始化:
1 | type Config struct { |
9. 一段综合示例
1 | package main |
输出:
1 | 姓名:小明 |
注意:示例中出现了方法 (s Student) AvgScore(),这是下一课的内容。这里先感受一下,不用担心。
10. 常见坑总结
10.1 以为结构体赋值是引用
1 | s1 := Student{Name: "小明"} |
结构体是值类型,赋值和传参都会完整复制。
10.2 字面量初始化遗漏字段
1 | s := Student{"小明", 20} // 编译错误:少了 Score |
用字面量初始化时,要么全部按顺序写,要么用字段名初始化。
10.3 nil 切片和 map 字段
1 | var s Student |
10.4 匿名嵌套字段名冲突
1 | type A struct { X int } |
当多个嵌入的结构体有同名字段时,必须用完整路径:c.A.X 或 c.B.X。
10.5 结构体太大时传值开销高
1 | type BigStruct struct { |
大结构体应该传指针:
1 | func process(s *BigStruct) { // 只复制 8 字节的指针 |
11. 本课练习
练习 1:定义和初始化
要求:
- 定义
Book结构体,包含Title、Author、Pages字段 - 用字段名初始化方式创建两本书
- 打印每本书的信息
练习 2:结构体切片
要求:
- 创建
[]Book,包含若干本书 - 用
for range遍历,打印每本的标题和作者 - 找出页数最多的书
练习 3:嵌套结构体
要求:
- 定义
ContactInfo结构体(Phone、Email) - 定义
Employee结构体(Name、ContactInfo) - 创建员工,通过嵌套字段访问联系信息
练习 4:传指针修改
要求:
- 定义函数
promote(e *Employee),给员工加薪(在结构体中加Salary字段) - 验证外部的结构体被修改
练习 5:结构体比较
要求:
- 定义
Point结构体(X,Y int) - 创建两个相同的
Point,用==比较 - 创建一个包含切片字段的结构体,尝试用
==比较,观察编译结果
练习 6:匿名嵌套
要求:
- 定义
Base结构体(含ID int和CreatedAt string) - 定义
Product嵌入Base,加上Name和Price - 验证可以通过
product.ID直接访问嵌入字段
12. 自测题
12.1 概念题
- 结构体怎么定义?
- 用字段名初始化和按顺序初始化,哪种更推荐?为什么?
- 结构体是值类型还是引用类型?
- 怎么在函数里修改外部的结构体?
- 结构体嵌套和嵌入有什么区别?
new(Student)返回什么?- 结构体赋值
s2 = s1是复制还是引用? - 哪些情况下结构体不能用
==比较? p.Name(p是指针)能直接访问字段吗?- 大结构体传参为什么建议传指针?
13. 本课总结
这一课你学到了 Go 中自定义类型的核心能力——结构体。
你现在应该已经理解:
- 结构体用
type Name struct { ... }定义 - 用字段名初始化最安全、最清晰
- 用点号
s.Field访问字段,指针也一样(语法糖) - 结构体是值类型:赋值和传参都复制
- 需要修改外部结构体时传指针
- 可以嵌套结构体(命名嵌套)或嵌入结构体(匿名嵌套)
- 所有字段可比较时,结构体可以用
==
14. 下一课预告
下一课我们学习:方法 method。
会重点讲:
- 方法是什么,和普通函数有什么区别
- 值接收者 vs 指针接收者
- 什么时候用值接收者,什么时候用指针接收者
- 方法的本质是什么
学完下一课,你就能为结构体定义行为,实现 Go 中"数据 + 行为"的组织方式。





