Go 结构体定义
在 Go 语言中,定义结构体 (Struct) 允许你创建自定义的数据类型,将相关的多个数据组合在一起。
通过结构体,你可以将现实世界中的实体或概念抽象为一个单一的单元,从而让代码变得更加易读、易维护且高效。
1. 认识结构体 (Struct)
结构体(Structure 的简写)是一种复合数据类型。它可以把零个或多个命名变量(称为“字段” Field)组合在一起,每个字段都可以有自己独立的数据类型。
虽然结构体看起来有点像其他面向对象编程语言中的“类 (Classes)”,但在 Go 语言中,结构体更加轻量级。Go 摒弃了传统的继承机制,专注于数据的表示(不过别担心,Go 通过“组合”机制同样能实现强大的复用,我们稍后会讲)。
1.1 定义结构体类型
要定义一个结构体,你需要使用 type 关键字,加上结构体的名字,再跟上 struct 关键字。在大括号 {} 内部,你就可以定义它的各个字段了(包含字段名和数据类型)。
package main
// 定义一个名为 Person 的结构体类型
type Person struct {
FirstName string
LastName string
Age int
}在这个例子中,我们定义了一个名为 Person 的结构体,它拥有三个字段:FirstName (字符串)、LastName (字符串) 和 Age (整数)。
1.2 结构体的零值 (Zero Values)
像 Go 语言里的其他数据类型一样,如果你在声明结构体变量时没有显式地给它赋初始值,Go 会自动将其所有字段初始化为对应类型的零值。
package main
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
}
func main() {
var p Person
// %+v 可以打印出结构体的字段名和对应的值
fmt.Printf("%+v\n", p) // 输出: {FirstName: LastName: Age:0}
}如你所见,字符串字段 FirstName 和 LastName 被初始化为了空字符串 "",而整数字段 Age 被初始化为了 0。
2. 创建与初始化结构体实例
在 Go 中,有多种方式可以创建和初始化结构体的实例。
2.1 使用结构体字面量 (Struct Literal)
这是日常开发中最常用、最直观的实例化方式。你可以在大括号内按名称为每个字段指定具体的值。
package main
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
}
func main() {
// 使用结构体字面量进行初始化
p := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
fmt.Printf("%+v\n", p) // 输出: {FirstName:John LastName:Doe Age:30}
}避坑指南:
你也可以省略字段名,直接按结构体定义时的顺序填入数值。但极其不推荐这种写法!因为它严重降低了代码的可读性,并且一旦未来结构体增加了新字段或调整了字段顺序,这段代码直接就会报错甚至引发隐蔽的 Bug。
// 极不推荐的写法,应尽量避免
p := Person{"John", "Doe", 30}2.2 使用 new 函数
我们在上一章学过,new 函数可以在内存中为一个新变量分配空间,并返回指向它的指针。用 new 创建的结构体,其字段全部为零值。
package main
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
}
func main() {
p := new(Person) // 返回的是 *Person (指针)
fmt.Printf("%+v\n", *p) // 输出: {FirstName: LastName: Age:0}
// 给字段赋值
p.FirstName = "John"
p.LastName = "Doe"
p.Age = 30
fmt.Printf("%+v\n", *p) // 输出: {FirstName:John LastName:Doe Age:30}
}2.3 使用构造函数 (Constructor Function)
Go 语言本身没有内置的“构造函数”关键字,但我们可以通过编写一个普通的函数来模拟构造函数的行为。这在需要进行复杂初始化逻辑,或者需要校验参数合法性时极其有用。惯例上,这类函数通常以 New 开头。
package main
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
}
// NewPerson 充当构造函数,返回结构体的指针
func NewPerson(firstName, lastName string, age int) *Person {
if age < 0 {
return nil // 参数校验:如果年龄不合法,返回 nil
}
return &Person{
FirstName: firstName,
LastName: lastName,
Age: age,
}
}
func main() {
p := NewPerson("John", "Doe", 30)
if p != nil {
fmt.Printf("创建成功: %+v\n", *p)
}
invalidPerson := NewPerson("Jane", "Doe", -5)
if invalidPerson == nil {
fmt.Println("提供的年龄无效,创建失败")
}
}返回结构体的指针 (*Person) 是一个好习惯,这不仅能在创建失败时返回 nil,还能避免复制整个结构体带来的性能开销。
3. 访问结构体字段
3.1 使用点号 (.) 操作符
你可以使用点号操作符轻松访问或修改结构体里的字段。
package main
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
}
func main() {
p := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
fmt.Println(p.FirstName) // 输出: John
fmt.Println(p.LastName) // 输出: Doe
p.Age = 31 // 修改字段的值
fmt.Println(p.Age) // 输出: 31
}3.2 通过指针访问字段 (自动隐式解引用)
如果你手里拿的是一个指向结构体的指针,你依然可以直接使用点号操作符来访问字段。Go 语言会在底层自动帮你完成指针的解引用操作,非常贴心。
package main
import "fmt"
type Person struct {
FirstName string
LastName string
Age int
}
func main() {
// 拿到 Person 的指针
p := &Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
// Go 自动将 p.FirstName 转译为 (*p).FirstName
fmt.Println(p.FirstName) // 输出: John
p.Age = 31 // 通过指针修改字段
fmt.Println(p.Age) // 输出: 31
}4. 结构体嵌套与组合 (Composition)
Go 语言虽然没有继承,但通过在结构体中嵌套另一个结构体,实现了强大且灵活的“组合 (Composition)”模式。你可以把已有的数据结构当作积木,拼装出更复杂的模型。
4.1 嵌套结构体字段 (Embedded Fields)
package main
import "fmt"
type Address struct {
Street string
City string
ZipCode string
}
type Person struct {
FirstName string
LastName string
Age int
Address Address // 将 Address 结构体作为一个常规字段嵌套进来
}
func main() {
p := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
Address: Address{
Street: "主街 123 号",
City: "某某市",
ZipCode: "12345",
},
}
fmt.Println(p.FirstName) // 输出: John
fmt.Println(p.Address.Street) // 逐级访问: 输出: 主街 123 号
fmt.Println(p.Address.City) // 输出: 某某市
}4.2 匿名字段与字段提升 (Anonymous Fields)
更高级的玩法是匿名嵌套。你可以直接把另一个结构体的类型名写进来,而不给它起字段名。
此时,被嵌套结构体的内部字段会被“提升 (Promoted)”到外层结构体中,你可以直接在外层结构体上访问它们!
package main
import "fmt"
type Address struct {
Street string
City string
ZipCode string
}
type Person struct {
FirstName string
LastName string
Age int
Address // 匿名嵌套:只写类型名,不写字段名
}
func main() {
p := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
Address: Address{
Street: "主街 123 号",
City: "某某市",
ZipCode: "12345",
},
}
fmt.Println(p.FirstName) // 输出: John
// ✨ 魔法发生:Street 和 City 被提升了,可以直接通过 p 访问!
fmt.Println(p.Street) // 输出: 主街 123 号
fmt.Println(p.City) // 输出: 某某市
// 当然,完整路径依然是通的
// fmt.Println(p.Address.Street)
}注意:如果内外层结构体出现了同名字段(冲突),外层的字段会优先覆盖内层的字段。为了代码清晰,建议尽量避免命名冲突。
5. 实战案例演示
示例 1:表示一个矩形
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
func main() {
r := Rectangle{
Width: 10.0,
Height: 5.0,
}
fmt.Println("宽度:", r.Width) // 输出: 宽度: 10
fmt.Println("高度:", r.Height) // 输出: 高度: 5
}示例 2:表示一本书及其作者信息 (组合模式)
package main
import "fmt"
type Author struct {
Name string
Bio string
}
type Book struct {
Title string
Author Author // 嵌套作者结构体
Pages int
}
func main() {
book := Book{
Title: "Go 语言程序设计",
Author: Author{
Name: "Alan Donovan & Brian Kernighan",
Bio: "Go 语言的经典权威著作。",
},
Pages: 384,
}
fmt.Println("书名:", book.Title) // 输出: 书名: Go 语言程序设计
fmt.Println("作者:", book.Author.Name) // 输出: 作者: Alan Donovan & Brian Kernighan
}