Go struct 详解:定义、初始化与嵌入字段 —— Build Web Application with Golang 第 2.4 节实战指南

原创2026-09-30 23:45:411,937 阅读
文章标签:文档教程后端

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 结构体的全部核心要点:

  1. 定义:type person struct { name string; age int },用字段组合出新的容器类型;
  2. 使用:点号运算符赋值与读取字段,字段零值由类型自动决定;
  3. 初始化:按顺序初始化、field:value 无序初始化、匿名结构体直接初始化三种方式;
  4. 按值传递:结构体作为函数参数时是复制传递,配合多返回值可以优雅地返回复合结果;
  5. 嵌入字段:结构体、命名类型乃至内置类型都可以嵌入,被嵌入结构体会隐式继承其全部字段;
  6. 字段冲突:外层字段优先,必要时可用 外层.嵌入类型.字段 的显式路径访问内层字段。

结合 下一节「面向对象」 的内容,你会发现嵌入字段正是 Go 实现类型复用与组合式设计的基础——它让 Student 天然"拥有" Human 的全部属性,而无需繁琐的继承体系。掌握了 struct 的这套机制,你就掌握了 Go 中组织数据模型的基石,这也是后续章节中定义 Web 数据模型、表结构映射与业务对象时反复使用的能力。

相关链接

登录后查看全文
build-web-application-with-golang