《Build Web Application with Golang》第 2.4 节精讲:Go 语言 struct 定义、初始化与字段嵌入机制
《Build Web Application with Golang》第 2.4 节精讲:Go 语言 struct 定义、初始化与字段嵌入机制
本文基于开源仓库
build-web-application-with-golang中西班牙语版第 2 章第 4 节(es/02.4.md)展开,并对照仓库en/code/src/apps/ch.2.4/下的可运行示例源码(compare_age、embedded_structs、embedded_structs2、embedded_structs_with_name_conflict)做源码级佐证。本节是通往第 2.5 节"面向对象"(Go 方法接收者)的必经基础:掌握 struct 定义、四种初始化写法、按值传递语义,以及 Go 特有的"嵌入字段"(embedded fields)组合复用机制,你就掌握了用 Go 组织数据模型的核心能力。
struct:Go 中自定义聚合类型的核心
Go 语言与 C 系语言一脉相承,同样允许我们像其他编程语言一样,定义一个"包含其他属性或字段的容器类型"。例如,我们可以创建一个名为 persona(person)的类型来表示一个人,它包含姓名(nombre)和年龄(edad)两个字段。这种自定义的聚合类型在 Go 中就叫 struct:
type persona struct {
nombre string
edad int
}
定义过程非常直观,type 关键字声明新类型,struct 关键字描述其字段集合。本例中有两个字段:
nombre:类型为string,用于保存人的姓名;edad:类型为int,用于保存人的年龄。
值得留意的是,struct 也出现在第 2 章开篇(es/02.0.md)列出的 25 个 Go 保留字(reserved words)清单中——Go 的全部语法仅由这 25 个关键字构成,甚至比英文字母表的 26 个字母还少,struct 正是其中最重要的数据组织关键字之一。
struct 的声明与字段访问
定义好类型之后,就可以声明变量并访问、修改其字段:
type persona struct {
nombre string
edad int
}
var P persona // P 的类型是 persona
P.nombre = "Astaxie" // 把 "Astaxie" 赋值给 P 的字段 'nombre'
P.edad = 25 // 把 25 赋值给 P 的字段 'edad'
fmt.Printf("El nombre de la persona es %s\n", P.nombre) // 通过 . 访问 P 的字段
可见 Go 的字段访问采用点号(.)语法:变量名.字段名 既可读取也可赋值。这里声明了 var P persona 之后,P 立即以零值可用(string 为零长度字符串,int 为 0),随后逐字段写入初值。
三种更便捷的初始化方式
除了先声明再逐字段赋值,Go 还提供了三种更紧凑的初始化(初始化器)写法,它们同样出现在仓库示例 main.go 对应的讲解流程中:
-
按顺序赋值初始化:按字段声明的顺序依次给出初值。
P := persona{"Tom", 25} -
使用
campo:valor(字段:值)格式、无需按顺序初始化:字段顺序任意,可读性更好,且可以只初始化部分字段。P := persona{edad: 24, nombre: "Bob"} -
定义匿名 struct 并同时初始化:不先
type声明,直接在表达式中写出struct{...}{...},适用于一次性使用的临时数据结构。P := struct { nombre string edad int }{"Amy", 18}
第三种写法在 Go 中被称为"匿名 struct",常见于测试数据构造或一次性响应体封装等场景。
完整示例:struct 按值传递
下面是一个完整可运行的程序,演示了如何比较两个人的年龄并返回较年长者与年龄差。注意注释中的关键语义:struct 是按值传递的(passed by value):
package main
import "fmt"
// 定义一个新类型
type persona struct {
nombre string
edad int
}
// 比较两个人的年龄,返回较年长者以及年龄差;struct 按值传递
func Older(p1, p2 persona) (persona, int) {
if p1.edad > p2.edad {
return p1, p1.edad - p2.edad
}
return p2, p2.edad - p1.edad
}
func main() {
var tom persona
// 初始化
tom.nombre, tom.edad = "Tom", 18
// 用 "campo:valor" 格式初始化两个值
bob := persona{edad: 25, nombre: "Bob"}
// 按顺序初始化两个值
paul := persona{"Paul", 43}
tb_Older, tb_diff := Older(tom, bob)
tp_Older, tp_diff := Older(tom, paul)
bp_Older, bp_diff := Older(bob, paul)
fmt.Printf("De %s y %s, %s es mas viejo por %d años\n", tom.nombre, bob.nombre, tb_Older.nombre, tb_diff)
fmt.Printf("De %s y %s, %s es mas viejo por %d años\n", tom.nombre, paul.nombre, tp_Older.nombre, tp_diff)
fmt.Printf("De %s y %s, %s es mas viejo por %d años\n", bob.nombre, paul.nombre, bp_Older.nombre, bp_diff)
}
程序输出三组两两比较结果。这里值得强调的底层语义:
- 按值传递:函数
Older接收的是p1、p2两个 struct 的副本,函数体内对字段的修改不会影响调用方的原变量。因此即使Older内部做任何字段操作,tom、bob、paul三个变量依然保持原始数据。 - 多返回值:
Older同时返回"较年长者"(persona)与"年龄差"(int),配合 Go 的多返回值赋值tb_Older, tb_diff := Older(...)直接解构,是本例中体现 Go 函数设计风格的典型写法。
这与仓库 compare_age/main.go 中给出的完整实现一致,可以 go run 直接验证输出。
嵌入字段(embedded fields):Go 的组合式复用
上面介绍的 struct 字段都"有名字也有类型"。实际上,Go 支持只有类型、没有名字的字段,这类字段被称为嵌入字段(embedded fields)。
当嵌入字段本身是一个 struct 时,该 struct 的所有字段会隐式地成为外层 struct 的字段。先看基础示例(对应仓库 embedded_structs/main.go):
package main
import "fmt"
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 嵌入字段:Student 将包含 Human 的所有字段
speciality string
}
func main() {
// 实例化并初始化一个 student
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// 字段访问
fmt.Println("Su nombre es ", mark.name)
fmt.Println("Su edad es ", mark.age)
fmt.Println("Su peso es ", mark.weight)
fmt.Println("Su especialidad es ", mark.speciality)
// 修改专业字段
mark.speciality = "AI"
fmt.Println("Mark cambio su especialidad")
fmt.Println("Su especialidad es ", mark.speciality)
// 修改年龄与体重
fmt.Println("Mark esta mas viejo")
mark.age = 46
mark.weight += 60
fmt.Println("Su edad es ", mark.age)
fmt.Println("Su peso es ", mark.weight)
}
图:Go 中通过嵌入字段实现的"继承"——Student 自动获得 Human 的全部字段(对应原文档 Figure 2.7)
可以看到,在 Student 上访问 mark.name、mark.age、mark.weight,与直接在 Human 上访问毫无二致,仿佛 Student "继承了" Human 的字段——这就是嵌入字段的工作方式。需要说明的是,Go 并没有传统 OOP 语言的类继承,这里实现的是一种基于组合的字段提升(field promotion),而这正是 Go 官方推荐的数据复用哲学。
更"酷"的是,你还能通过外层类型直接访问并整体替换被嵌入的那个 struct:
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1
即先整体替换嵌入的 Human 实例,再通过 mark.Human.age 这一中间路径精确修改其中的字段。
任意类型都可作为嵌入字段
嵌入字段并不局限于 struct——Go 中所有类型都可以作为嵌入字段。例如下面这个例子(对应仓库 embedded_structs2/main.go),一个 Student 同时嵌入了 struct(Human)、自定义切片类型(Skills)以及内建类型(int):
package main
import "fmt"
type Skills []string
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // struct 作为嵌入字段
Skills // 字符串切片类型作为嵌入字段
int // 内建类型 int 作为嵌入字段
speciality string
}
func main() {
// 初始化 Student Jane
jane := Student{Human: Human{"Jane", 35, 100}, speciality: "Biology"}
// 访问字段
fmt.Println("Su nombre es ", jane.name)
fmt.Println("Su edad es ", jane.age)
fmt.Println("Su peso es ", jane.weight)
fmt.Println("Su especialidad es ", jane.speciality)
// 修改技能字段的值
jane.Skills = []string{"anatomy"}
fmt.Println("Sus habilidades son ", jane.Skills)
fmt.Println("Ella adquirió dos habilidades mas")
jane.Skills = append(jane.Skills, "physics", "golang")
fmt.Println("Sus habilidades ahora son ", jane.Skills)
// 修改内建类型嵌入字段
jane.int = 3
fmt.Println("Su numero preferido es ", jane.int)
}
关键点总结:
jane.Skills本身就是一个[]string,可以直接赋值,也可以直接用标准库函数append追加元素("physics"、"golang")——嵌入字段与普通字段在用法上完全一致;jane.int直接作为一个int字段被读写,证明内建类型同样可以嵌入;- 初始化时,嵌入字段使用类型名作为键(如
Human: Human{...}),与"字段:值"格式一脉相承。
字段名冲突:外层字段优先(字段"重载")
最后一个问题:如果 Human 有一个名为 phone 的字段,而 Student/Employee 中恰好也有同名字段,会发生什么?
Go 用非常简洁的规则解决:外层字段获得更高访问优先级(outer fields get upper access levels)。也就是说,访问 student.phone 得到的是 Student/Employee 自己的 phone 字段,而不是嵌入的 Human 里的 phone。这一特性可以通俗地理解为字段的"重载"(overloading)。对应仓库源码 embedded_structs_with_name_conflict/main.go:
package main
import "fmt"
type Human struct {
name string
age int
phone string // Human 拥有 phone 字段
}
type Employee struct {
Human // 嵌入字段 Human
speciality string
phone string // employee 自己的 phone 字段
}
func main() {
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
fmt.Println("El teléfono del trabajo de Bob es:", Bob.phone)
// 访问 Human 中的 phone 字段
fmt.Println("El teléfono personal de Bob es:", Bob.Human.phone)
}
输出结果:
El teléfono del trabajo de Bob es: 333-222
El teléfono personal de Bob es: 777-444-XXXX
也就是说:
Bob.phone命中Employee自己声明的phone(工作电话);- 若要访问被遮蔽的嵌入字段,必须显式写出完整路径
Bob.Human.phone(个人电话)。
遮蔽(shadowing)而不是覆盖是理解这一规则的关键:内层的 Human.phone 依然存在且可访问,只是在外层同名冲突时让位于外层字段。如果未来你需要在应用中保留"个人电话/工作电话"这类同名字段的数据模型,这就是 Go 给出的标准答案。
与仓库源码的对照与运行方式
本节全部示例均可在仓库的英文版示例代码目录下找到完整可运行版本,作为西班牙语文档的源码级佐证:
| 文档讲解主题 | 对应仓库源码 |
|---|---|
| 年龄比较与按值传递 | en/code/src/apps/ch.2.4/compare_age/main.go |
| 基础 struct 定义与初始化 | en/code/src/apps/ch.2.4/main.go |
| 嵌入 struct 字段 | en/code/src/apps/ch.2.4/embedded_structs/main.go |
| 嵌入任意类型(Skills、int) | en/code/src/apps/ch.2.4/embedded_structs2/main.go |
| 字段名冲突与外层优先 | en/code/src/apps/ch.2.4/embedded_structs_with_name_conflict/main.go |
运行验证方法:在安装好 Go 工具链的环境下,进入对应目录执行 go run main.go 即可看到与上文描述一致的控制台输出。若想按本仓库推荐的开发方式运行,可参考 es/code/readme.md 的设置建议,将 GOPATH 指向代码目录,从而在任何路径下都能编译运行示例。
小结
本节围绕 Go 的 struct 建立了一条完整的学习脉络:
- 定义:
type X struct { ... }声明字段集合; - 访问:点号
.语法读写字段; - 初始化:顺序初始化、
字段:值初始化、匿名 struct 初始化三种便捷写法; - 传递语义:struct 按值传递,函数内修改不影响调用方;
- 嵌入字段:struct、自定义类型、内建类型均可嵌入,字段自动"提升";
- 同名冲突:外层字段优先,内层字段通过完整路径显式访问。
理解嵌入字段的"组合优于继承"思路后,下一步就可以进入第 2.5 节"面向对象",学习如何给 struct 绑定方法(接收者 receiver),届时你会看到嵌入字段如何进一步把方法也一并"继承"过来。本章完整导航见目录,前一节可回顾"控制语句与函数"。
(本文内容整理自仓库文档 es/02.4.md,并与 en/02.4.md 及 en/code/src/apps/ch.2.4/ 源码相互印证。)
