- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
本指南对应开源仓库《Build Web Application with Golang》中文版 第 2.4 节,系统讲解 Go 语言 struct 类型的声明、四种初始化方式、匿名字段(嵌入字段)的继承机制、字段重载与命名冲突处理。无论你是在搭建 Web 应用的模型层、定义数据库实体结构,还是封装请求/响应对象,掌握 struct 都是 Go 开发的第一步。读完本文,你将能熟练使用字段式/顺序式初始化、利用嵌入字段实现组合复用,并正确处理字段同名冲突——这些能力直接支撑后续章节中的面向对象与 Web 框架实践。
为什么需要 struct:为数据建模而生的容器类型
Go 语言中,和 C 或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型person代表一个人的实体,这个实体拥有属性:姓名和年龄。这样的类型我们称之为struct。如下代码所示:
type person struct { name string age int }声明一个 struct 非常简单,上面的类型包含有两个字段:
- 一个
string类型的字段name,用来保存用户名称这个属性; - 一个
int类型的字段age,用来保存用户年龄这个属性。
在后续的 Web 开发中,struct 常被用来描述业务实体——例如用户、文章、订单——或者承载 HTTP 请求的表单数据结构。这一小节打好基础,后面的面向对象与接口 interface章节都建立在此之上。
struct 的使用:声明变量、访问与修改字段
如何使用 struct 呢?请看下面的代码:
type person struct { name string age int } var P person // P现在就是person类型的变量了 P.name = "Astaxie" // 赋值"Astaxie"给P的name属性. P.age = 25 // 赋值"25"给变量P的age属性 fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.这里var P person声明了一个person类型的零值变量,随后通过P.name = ...、P.age = ...逐字段赋值,最后用P.name读取属性。注意:struct 是值类型,赋给新变量或传给函数时会发生整体拷贝,下文Older函数的例子会再次印证这一点。
struct 的四种初始化方式
除了上面这种先声明后逐字段赋值的方式,还有另外几种声明使用方式:
按照顺序提供初始化值:
P := person{"Tom", 25}这种方式要求值的个数、类型与字段声明顺序严格一致,即第一个值对应
name,第二个值对应age。通过
field:value的方式初始化,这样可以任意顺序:P := person{age:24, name:"Tom"}字段名加冒号的形式不要求与声明顺序一致,可读性更强,推荐在字段较多的结构体上使用。
通过
new函数分配一个指针,此处 P 的类型为*person:P := new(person)new返回指向零值结构体的指针,配合后面的P.name = ...等赋值语句使用,常用于需要传递指针或延迟初始化的场景。
在仓库的配套示例代码 en/code/src/apps/ch.2.4/main.go 中,上述初始化方式被集中演示,还额外展示了匿名 struct 的用法:
func show_basic_struct() { type person struct { name string age int } var P person // p is person type P.name = "Astaxie" // assign "Astaxie" to the filed 'name' of p P.age = 25 // assign 25 to field 'age' of p fmt.Printf("The person's name is %s\n", P.name) tom := person{"Tom", 25} bob := person{age: 24, name: "Bob"} fmt.Printf("tom = %+v\n", tom) fmt.Printf("bob = %#v\n", bob) } func show_anonymous_struct() { fmt.Printf("Anonymous struct = %#v\n", struct { name string count int }{"counter", 1}) }其中%+v会打印带字段名的值,%#v会打印带类型名与字段名的完整 Go 语法表示,非常适合调试时观察结构体内容。
完整示例:struct 作为函数参数按值传递
下面我们看一个完整的使用 struct 的例子:
package main import "fmt" // 声明一个新的类型 type person struct { name string age int } // 比较两个人的年龄,返回年龄大的那个人,并且返回年龄差 // struct也是传值的 func Older(p1, p2 person) (person, int) { if p1.age>p2.age { // 比较p1和p2这两个人的年龄 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"} // 按照struct定义顺序初始化值 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先声明再通过多重赋值tom.name, tom.age = "Tom", 18初始化;bob使用field:value方式初始化(顺序与声明相反);paul使用按声明顺序的位置参数方式初始化;Older函数接收两个person参数并返回(person, int)。注释里特别写明“struct 也是传值的”,意味着函数内部对p1、p2的任何修改都不会影响调用方的原始数据。如果希望函数内修改结构体字段并影响外部,需要传递指针(*person),这一点在后面的函数与指针和面向对象章节中会进一步展开。
配套源码 en/code/src/apps/ch.2.4/compare_age/main.go 中保存着这一示例的可运行版本。
struct 的匿名字段:实现字段“继承”的组合机制
我们上面介绍了如何定义一个 struct,定义的时候是字段名与其类型一一对应,实际上 Go 支持只提供类型、而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
当匿名字段是一个 struct 的时候,那么这个 struct 所拥有的全部字段都被隐式地引入了当前定义的这个 struct。
让我们来看一个例子,让上面说的这些更具体化:
package main import "fmt" type Human struct { name string age int weight int } type Student struct { Human // 匿名字段,那么默认Student就包含了Human的所有字段 speciality string } func main() { // 我们初始化一个学生 mark := Student{Human{"Mark", 25, 120}, "Computer Science"} // 我们访问相应的字段 fmt.Println("His name is ", mark.name) fmt.Println("His age is ", mark.age) fmt.Println("His weight is ", mark.weight) fmt.Println("His speciality is ", mark.speciality) // 修改对应的备注信息 mark.speciality = "AI" fmt.Println("Mark changed his speciality") fmt.Println("His speciality is ", mark.speciality) // 修改他的年龄信息 fmt.Println("Mark become old") mark.age = 46 fmt.Println("His age is", mark.age) // 修改他的体重信息 fmt.Println("Mark is not an athlet anymore") mark.weight += 60 fmt.Println("His weight is", mark.weight) }图 2.7:struct 组合——Student 组合了 Human struct 和 string 基本类型
我们看到 Student 访问属性age和name的时候,就像访问自己所有的字段一样。对,匿名字段就是这样,能够实现字段的继承。还有比这个更酷的:Student 还能把Human这个字段名当作字段访问。请看下面的代码:
mark.Human = Human{"Marcus", 55, 220} mark.Human.age -= 1也就是说,嵌入字段既可以通过字段名直接访问(mark.Human是Human类型的值),也可以忽略字段名直接访问其内部字段(mark.age),并且都可以赋值和修改。通过匿名访问和修改字段相当的有用。
配套源码 en/code/src/apps/ch.2.4/embedded_structs/main.go 保存了上述 Student 组合 Human 的完整可运行版本。
不仅仅是 struct:自定义类型与内置类型都可以作为匿名字段
匿名字段的妙处不止于此——不仅仅是 struct 字段,所有的内置类型和自定义类型都可以作为匿名字段。请看下面的例子:
package main import "fmt" type Skills []string type Human struct { name string age int weight int } type Student struct { Human // 匿名字段,struct Skills // 匿名字段,自定义的类型string slice int // 内置类型作为匿名字段 speciality string } func main() { // 初始化学生Jane jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"} // 现在我们来访问相应的字段 fmt.Println("Her name is ", jane.name) fmt.Println("Her age is ", jane.age) fmt.Println("Her weight is ", jane.weight) fmt.Println("Her speciality is ", jane.speciality) // 我们来修改他的skill技能字段 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) // 修改匿名内置类型字段 jane.int = 3 fmt.Println("Her preferred number is", jane.int) }从上面例子我们可以看出:
- struct 能够将 struct 作为匿名字段(
Human); - 自定义类型可以作为匿名字段(
Skills,其本质是[]string); - 内置类型也可以作为匿名字段(
int); - 而且可以在相应字段上进行函数操作,如例子中的
append(jane.Skills, "physics", "golang"),说明匿名字段拥有其类型本身的全部能力。
注意初始化时Student{Human: Human{"Jane", 35, 100}, speciality: "Biology"}使用了带字段名的复合字面量——由于Skills和int也是匿名字段,它们同样可以作为字段名出现在初始化列表中(例如Skills: []string{...}、int: 3)。这里省略了Skills与int,它们保持各自的零值(nil与0)。
配套源码 en/code/src/apps/ch.2.4/embedded_structs2/main.go 保存了该示例的可运行版本。
字段重载与命名冲突:最外层优先访问
这里有一个问题:如果 Human 里面有一个字段叫做phone,而 Student(本例中为 Employee)也有一个字段叫做phone,那么该怎么办呢?
Go 里面很简单地解决了这个问题:最外层的优先访问。也就是说,当你通过student.phone访问的时候,访问的是 student(最外层)里面的字段,而不是 human 里面的字段。
这样就允许我们去重载通过匿名字段继承的一些字段。当然,如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。请看下面的例子:
package main import "fmt" type Human struct { name string age int phone string // Human类型拥有的字段 } type Employee struct { Human // 匿名字段Human speciality string phone string // 雇员的phone字段 } func main() { Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"} fmt.Println("Bob's work phone is:", Bob.phone) // 如果我们要访问Human的phone字段 fmt.Println("Bob's personal phone is:", Bob.Human.phone) }运行结果中Bob.phone输出的是雇员的工作电话333-222,而Bob.Human.phone才能拿到 Human 里的个人电话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 保存了该字段冲突示例的可运行版本。
需要提醒的是,匿名字段冲突只会影响同层级的字段访问;如果匿名字段的层级不同(例如在嵌套组合中一个字段在更深层级),Go 仍会优先选择最浅层级的字段。这一“浅层优先”规则与“外层优先”规则一起,构成了 Go 嵌入字段解析的完整行为,在实际建模时可以放心使用组合来扩展类型,而不必担心字段互相污染。
小结
本节我们完整学习了 Go struct 的核心知识:
- 声明:
type person struct { ... },为业务实体建模; - 访问与修改:通过点操作符读写字段;
- 四种初始化:位置参数、
field:value、new指针、先声明后赋值; - 按值传递:struct 作为函数参数时是值传递,注意区分指针语义;
- 匿名字段(嵌入字段):struct、自定义类型、内置类型均可嵌入,实现字段的隐式“继承”与组合复用;
- 字段重载与冲突:最外层字段优先访问,通过匿名字段名可显式访问内层字段。
这些能力是后续面向对象(方法与接收者)、接口 interface乃至整个 Web 应用开发的基础。仓库中所有示例均可在 en/code/src/apps/ch.2.4/ 目录下找到可运行的 Go 源码,建议逐一运行并修改字段值,亲手验证本节所述的初始化方式与嵌入规则。
- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web with golang
相关推荐
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 节精讲:Go 语言 struct 定义、初始化与字段嵌入机制
《Build Web Application with Golang》第 2.4 节精讲:Go 语言 struct 定义、初始化与字段嵌入机制 本文基于开源仓库
文档教程Go 语言 struct 完全指南:定义、初始化、嵌入字段与名称遮蔽(build-web-application-with-golang 第 2.4 节)
Go 语言 struct 完全指南:定义、初始化、嵌入字段与名称遮蔽(build web application with golang 第 2.4 节) 本篇
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考