Go struct 详解:定义、初始化与嵌入字段 —— Build Web Application with Golang 第 2.4 节实战指南
Go struct 详解:定义、初始化与嵌入字段 —— Build Web Application with Golang 第 2.4 节实战指南
本篇技术指南基于开源电子书《Build Web Application with Golang》第 2 章第 4 节(en/02.4.md)展开,系统讲解 Go 语言中**结构体(struct)的定义、字段访问、三种初始化方式、按值传递特性,以及 Go 特有的嵌入字段(embedded fields)**机制——包括任意类型嵌入与字段名冲突的覆盖规则。你将学会如何用 struct 组织数据模型,并借助仓库内配套的可运行示例(en/code/src/apps/ch.2.4)亲手验证每一个知识点,为后续章节的 Web 开发、面向对象编程打下扎实基础。
struct 是什么:定义带字段的自定义容器类型
与大多数编程语言一样,Go 允许我们定义一种"由若干属性或字段组合而成"的新类型容器,这种类型就叫 struct。例如,创建一个名为 person 的类型来表示一个人,它包含 name 和 age 两个字段:
type person struct {
name string
age int
}
定义结构体的语法非常简洁:type 类型名 struct { ... }。上述示例中包含了两个字段:
name:string类型,用于存储一个人的姓名;age:int类型,用于存储一个人的年龄。
需要说明的是,这里定义的 person 只是类型声明,尚未创建任何实际的数据。真正使用它,需要先声明变量再赋值,或直接初始化。
结构体的基本使用:声明、赋值与字段访问
结构体变量的字段可以通过点号(.)运算符进行赋值和读取。下面的代码演示了最基础的用法:
type person struct {
name string
age int
}
var P person // p is person type
P.name = "Astaxie" // assign "Astaxie" to the field 'name' of p
P.age = 25 // assign 25 to field 'age' of p
fmt.Printf("The person's name is %s\n", P.name) // access field 'name' of p
这里先用 var P person 声明一个 person 类型的变量(此时字段为零值:name 为空字符串、age 为 0),再逐字段赋值,最后通过 P.name 读取字段并打印。
三种初始化方式
除了"先声明、后赋值"之外,Go 还提供了三种更简洁的结构体初始化方式:
1. 按字段顺序依次给初始值
P := person{"Tom", 25}
这种方式要求初始值的个数、顺序和类型必须与结构体定义完全一致:第一个值 "Tom" 对应 name,第二个值 25 对应 age。
2. 使用 field:value 格式,不要求顺序
P := person{age:24, name:"Bob"}
这种方式通过"字段名:值"显式指定,因此可以任意调整书写顺序,可读性更好,也更不容易出错——即使日后结构体新增字段,这种初始化方式也不会因为位置错位而悄悄赋错值。
3. 定义匿名结构体后立即初始化
P := struct{name string; age int}{"Amy",18}
如果某个结构体类型只在当前作用域临时使用一次,可以直接在表达式中定义匿名结构体(即不带类型名)并马上初始化,无需额外 type 声明。
仓库中的 en/code/src/apps/ch.2.4/main.go 把上述几种初始化方式整合在一个可运行程序中,并用 %+v 与 %#v 两种格式化输出展示结果,非常适合对照验证:
tom := person{"Tom", 25}
bob := person{age: 24, name: "Bob"}
fmt.Printf("tom = %+v\n", tom) // tom = {name:Tom age:25}
fmt.Printf("bob = %#v\n", bob) // bob = main.person{name:"Bob", age:24}
其中 %+v 会打印字段名与值,%#v 会打印完整的类型信息与字段名,是调试结构体的常用手段。
完整示例:结构体按值传递
下面的完整程序同时演示了结构体的多种初始化方式、作为函数参数按值传递(pass by value)的特性,以及多返回值:
package main
import "fmt"
// define a new type
type person struct {
name string
age int
}
// struct is passed by value
// compare the age of two people, then return the older person and differences of age
func Older(p1, p2 person) (person, int) {
if p1.age > p2.age {
return p1, p1.age - p2.age
}
return p2, p2.age - p1.age
}
func main() {
var tom person
tom.name, tom.age = "Tom", 18
bob := person{age: 25, name: "Bob"}
paul := person{"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("Of %s and %s, %s is older by %d years\n", tom.name, bob.name, tb_Older.name, tb_diff)
fmt.Printf("Of %s and %s, %s is older by %d years\n", tom.name, paul.name, tp_Older.name, tp_diff)
fmt.Printf("Of %s and %s, %s is older by %d years\n", bob.name, paul.name, bp_Older.name, bp_diff)
}
这段代码的要点:
- 三个人分别用三种不同方式初始化:
tom先声明后赋值,bob用field:value,paul按顺序初始化; Older(p1, p2 person)接收两个person结构体参数并返回"年龄更大的人"与"年龄差"两个值,这正是 Go 多返回值的典型应用;- 注释明确说明"struct is passed by value"——结构体作为参数传入时是按值复制的,函数内部对参数的修改不会影响调用方的原变量。如果你希望修改原结构体,需要显式使用指针(
*person),这将在后续章节进一步展开。
程序输出结果为:
Of Tom and Bob, Bob is older by 7 years
Of Tom and Paul, Paul is older by 25 years
Of Bob and Paul, Paul is older by 18 years
仓库中对应的完整可运行源码位于 en/code/src/apps/ch.2.4/compare_age/main.go。
嵌入字段(embedded fields):没有名字的字段
前面定义结构体时,每个字段都有字段名和类型。实际上,Go 支持只写类型、不写字段名的字段,这种字段称为嵌入字段(embedded fields,也叫匿名字段 anonymous fields)。
当嵌入字段本身是一个结构体时,被嵌入结构体(如 Student)会隐式拥有嵌入结构体(如 Human)的全部字段,仿佛这些字段直接定义在 Student 中一样:
package main
import "fmt"
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // embedded field, it means Student struct includes all fields that Human has.
specialty string
}
func main() {
// instantiate and initialize a student
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// access fields
fmt.Println("His name is ", mark.name)
fmt.Println("His age is ", mark.age)
fmt.Println("His weight is ", mark.weight)
fmt.Println("His specialty is ", mark.specialty)
// modify mark's specialty
mark.specialty = "AI"
fmt.Println("Mark changed his specialty")
fmt.Println("His specialty is ", mark.specialty)
fmt.Println("Mark become old. He is not an athlete anymore")
mark.age = 46
mark.weight += 60
fmt.Println("His age is", mark.age)
fmt.Println("His weight is", mark.weight)
}

图 2.7:Student 与 Human 的嵌入关系示意(图片来源:en/images/2.4.student_struct.png)
可以看出,Student 中虽然没有显式定义 name、age、weight 字段,但我们直接通过 mark.name、mark.age、mark.weight 就能读写它们,与访问 Human 中的字段完全一致。这就是嵌入字段的工作方式。
更妙的是,我们还可以通过类型名直接操作嵌入的整个结构体:
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1
这里先把 mark 中嵌入的整个 Human 结构体重置为一个新值,再对其中的 age 字段做自减运算。通过 mark.Human 我们可以按"命名空间"方式精确访问被嵌入的那一层结构体。
仓库对应示例:en/code/src/apps/ch.2.4/embedded_structs/main.go。
任意类型都可以作为嵌入字段
嵌入字段不限于结构体——Go 中所有类型都可以作为嵌入字段,包括命名类型(如自定义的切片类型)和内置类型(如 int)。下面的例子综合展示了这一点:
package main
import "fmt"
type Skills []string
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // struct as embedded field
Skills // string slice as embedded field
int // built-in type as embedded field
specialty string
}
func main() {
// initialize Student Jane
jane := Student{Human: Human{"Jane", 35, 100}, specialty: "Biology"}
// access fields
fmt.Println("Her name is ", jane.name)
fmt.Println("Her age is ", jane.age)
fmt.Println("Her weight is ", jane.weight)
fmt.Println("Her specialty is ", jane.specialty)
// modify value of skill field
jane.Skills = []string{"anatomy"}
fmt.Println("Her skills are ", jane.Skills)
fmt.Println("She acquired two new ones ")
jane.Skills = append(jane.Skills, "physics", "golang")
fmt.Println("Her skills now are ", jane.Skills)
// modify embedded field
jane.int = 3
fmt.Println("Her preferred number is ", jane.int)
}
这段代码值得注意的细节:
Skills是一个以[]string为基础定义的新类型,作为嵌入字段后,jane.Skills可以直接使用切片操作:先整体赋值,再用append追加"physics"和"golang";int这个内置类型也被作为嵌入字段,因此jane.int是一个可直接读写的整型字段——这展示了嵌入机制的通用性;- 初始化时使用
Human: Human{"Jane", 35, 100}这种"字段名:值"的方式显式指定嵌入字段,更清晰。
仓库对应示例:en/code/src/apps/ch.2.4/embedded_structs2/main.go。
字段名冲突:外层字段拥有更高访问优先级
如果 Human 中有一个 phone 字段,而 Student(或 Employee)中恰好也有一个同名字段,会发生什么?Go 用一条非常简单的规则解决了这个问题:外层的字段拥有更高的访问优先级。也就是说,当你访问 student.phone 时,得到的是 Student 自己的 phone 字段,而不是嵌入的 Human 结构体里的 phone。这个特性可以简单理解为字段的"重载"(overloading):
package main
import "fmt"
type Human struct {
name string
age int
phone string // Human has phone field
}
type Employee struct {
Human
specialty string
phone string // phone in employee
}
func main() {
Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"}
fmt.Println("Bob's work phone is:", Bob.phone)
fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}
运行结果:
Bob's work phone is: 333-222
Bob's personal phone is: 777-444-XXXX
Bob.phone 命中外层 Employee 自己的 phone(工作电话),而通过 Bob.Human.phone 可以绕过遮蔽,精确访问嵌入层 Human 里的 phone(个人电话)。这种"外层优先 + 显式路径可穿透"的设计,既避免了命名冲突的歧义,又保留了访问内层字段的通道。
仓库对应示例:en/code/src/apps/ch.2.4/embedded_structs_with_name_conflict/main.go。
动手验证:运行仓库配套示例
本仓库为 2.4 节准备了五个可独立运行的示例程序,全部位于 en/code/src/apps/ch.2.4 目录:
| 示例目录 | 对应知识点 | 关键输出 |
|---|---|---|
main.go |
三种初始化方式与格式化打印 | %+v / %#v 输出 |
compare_age/main.go |
结构体按值传递、多返回值 | 三人年龄比较结果 |
embedded_structs/main.go |
结构体嵌入字段 | 直接访问 Human 字段 |
embedded_structs2/main.go |
任意类型嵌入(切片、内置类型) | Skills 追加、int 字段 |
embedded_structs_with_name_conflict/main.go |
字段名冲突与外层优先规则 | 工作电话 / 个人电话 |
运行方式:根据 en/code/readme.md 的建议,将环境变量 GOPATH 设置为 en/code 目录,然后从任意位置进入对应子目录执行 go run main.go 即可。例如:
export GOPATH=/path/to/repo/en/code
cd en/code/src/apps/ch.2.4/compare_age
go run main.go
小结
本章(第 2.4 节)覆盖了 Go 结构体的全部核心要点:
- 定义:
type person struct { name string; age int },用字段组合出新的容器类型; - 使用:点号运算符赋值与读取字段,字段零值由类型自动决定;
- 初始化:按顺序初始化、
field:value无序初始化、匿名结构体直接初始化三种方式; - 按值传递:结构体作为函数参数时是复制传递,配合多返回值可以优雅地返回复合结果;
- 嵌入字段:结构体、命名类型乃至内置类型都可以嵌入,被嵌入结构体会隐式继承其全部字段;
- 字段冲突:外层字段优先,必要时可用
外层.嵌入类型.字段的显式路径访问内层字段。
结合 下一节「面向对象」 的内容,你会发现嵌入字段正是 Go 实现类型复用与组合式设计的基础——它让 Student 天然"拥有" Human 的全部属性,而无需繁琐的继承体系。掌握了 struct 的这套机制,你就掌握了 Go 中组织数据模型的基石,这也是后续章节中定义 Web 数据模型、表结构映射与业务对象时反复使用的能力。
相关链接
- 目录
- 上一节:控制语句与函数
- 下一节:面向对象
- 配套源码:en/code/src/apps/ch.2.4