Files
notes/resource/go/Go 语言的结构体(Struct).md
T
2026-03-01 01:43:46 +08:00

490 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 1. 结构体定义与初始化
结构体通过 `type``struct` 关键字定义。初始化实例有多种灵活的方式。
## 1.1 定义语法
```go
type StructName struct {
FieldName1 FieldType1
FieldName2 FieldType2
// ...
}
```
## 1.2 初始化实例
以下是四种最常见的初始化结构体的方式,每种方式都有其适用的场景。
```go
// [代码规范化]:这是一个完整的、可运行的示例,整合了所有初始化方式。
package main
import "fmt"
// Person 定义了一个代表“人”的结构体
type Person struct {
name string
age int
}
func main() {
fmt.Println("--- 结构体初始化示例 ---")
// 方式1:先声明,后赋值 (零值初始化)
// 声明一个 Person 类型的变量 p1,此时所有字段都是其类型的零值。
// string 的零值是 ""int 的零值是 0。
var p1 Person
p1.name = "张三"
p1.age = 25
fmt.Printf("方式1 - 声明后赋值: %+v\n", p1) // %+v 可以打印字段名和值
// 方式2:使用字面量并指定字段名 (推荐)
// 这是最常用、最清晰、最不易出错的方式,因为字段顺序的改变不影响初始化。
p2 := Person{
name: "李四",
age: 30,
}
fmt.Printf("方式2 - 字段名初始化: %+v\n", p2)
// 方式3:使用字面量,按顺序提供字段值
// [补充知识点] 常见陷阱:这种方式代码脆弱,如果结构体定义中字段顺序改变,
// 此处的初始化代码必须同步修改,否则会导致编译错误或逻辑错误。
p3 := Person{"王五", 35}
fmt.Printf("方式3 - 顺序初始化: %+v\n", p3)
// 方式4:使用 new 关键字创建结构体指针 (详见下一节)
// new(Person) 会分配内存,并将所有字段初始化为零值,然后返回一个指向该内存的指针。
p4 := new(Person)
p4.name = "赵六"
p4.age = 40
fmt.Printf("方式4 - 使用 new 创建指针: %+v\n", *p4) // 注意需要解引用
}
```
# 2. 结构体指针 (Struct Pointers)
在 Go 中,直接传递结构体会产生一次完整的内存拷贝。对于大型结构体,这会影响性能。使用结构体指针可以避免拷贝,并且允许函数直接修改原始结构体实例。
```go
// [代码规范化]:这是一个完整的、可运行的示例,清晰地对比了值传递和指针传递。
package main
import "fmt"
// Student 定义了一个学生结构体
type Student struct {
name string
age int
}
// updateAgeByValue 接收一个结构体值(副本)
// 在函数内对 s 的任何修改都不会影响原始的 Student 变量。
func updateAgeByValue(s Student, newAge int) {
s.age = newAge
fmt.Printf("-> 在值传递函数内,年龄被修改为: %d\n", s.age)
}
// updateAgeByPointer 接收一个结构体指针
// 在函数内对 s 的修改会直接作用于原始的 Student 变量。
func updateAgeByPointer(s *Student, newAge int) {
// Go 语言提供了语法糖,可以直接用 s.age 访问,
// 无需写成 (*s).age。编译器会自动处理。
s.age = newAge
fmt.Printf("-> 在指针传递函数内,年龄被修改为: %d\n", s.age)
}
func main() {
fmt.Println("--- 结构体指针与值传递对比 ---")
// 场景1: 值传递
s1 := Student{name: "小明", age: 18}
fmt.Printf("原始 s1: %+v\n", s1)
updateAgeByValue(s1, 20)
fmt.Printf("调用值传递函数后,原始 s1: %+v (未改变)\n\n", s1)
// 场景2: 指针传递
s2 := &Student{name: "小红", age: 22} // 使用 & 获取结构体实例的指针
fmt.Printf("原始 s2: %+v\n", *s2)
updateAgeByPointer(s2, 25)
fmt.Printf("调用指针传递函数后,原始 s2: %+v (已改变)\n", *s2)
}
```
# 3. 结构体方法 (Methods)
方法是附加到特定类型(接收者)上的函数。Go 使用方法为结构体添加行为。
## 3.1 值接收者 vs. 指针接收者
方法的接收者可以是值类型,也可以是指针类型,选择哪种类型是 Go 设计中的一个重要考量点。
| 特性 | 值接收者 (`func (r MyType) Method()`) | 指针接收者 (`func (r *MyType) Method()`) |
| :--- | :--- | :--- |
| **操作对象** | 结构体的**副本** | 结构体的**原始实例** |
| **修改能力** | ❌ **不能**修改原始结构体 | ✅ **可以**修改原始结构体 |
| **性能开销** | 存在数据拷贝,对大结构体开销较大 | 无拷贝开销,仅传递指针(内存地址) |
| **NIL 值** | 不能用于 `nil` 接收者 | 可以为 `nil` 接收者定义方法(需在方法内做 `nil` 检查) |
| **调用** | Go 编译器会自动进行值和指针的转换,调用方便 | Go 编译器会自动进行值和指针的转换,调用方便 |
**[补充知识点] 最佳实践:如何选择接收者类型?**
1. **首选指针接收者**
- **修改状态**:当方法需要修改接收者的字段时,必须使用指针。
- **性能**:为避免大结构体的复制开销,即使不修改状态,也推荐使用指针。
- **一致性**:如果一个类型已经有了指针接收者的方法,那么其他方法也应保持一致,使用指针接收者,以避免混淆。
2. **何时使用值接收者**
- **不可变性**:当你想保证该方法不会修改接收者时。
- **小结构体**:对于非常小的、不包含指针或 slice 等引用类型的结构体(如 `type Point struct{X, Y int}`),值接收者的拷贝开销可以忽略不计。
- **并发安全**:创建副本可以天然地避免在并发环境下对共享数据的意外修改(但通常有更好的并发控制方法)。
```go
package main
import "fmt"
// Rectangle 定义了矩形结构体
type Rectangle struct {
width float64
height float64
}
// Area 是一个值接收者方法
// 它在 Rectangle 的副本上操作,因此是安全的,不会修改原始矩形。
func (r Rectangle) Area() float64 {
return r.width * r.height
}
// Scale 是一个指针接收者方法
// 它需要修改矩形的尺寸,所以必须使用指针接收者。
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}
func main() {
fmt.Println("--- 结构体方法示例 ---")
rect := Rectangle{width: 10, height: 5}
fmt.Printf("原始矩形: %+v, 面积: %.2f\n", rect, rect.Area())
// 调用指针接收者方法。Go 自动将 rect 转换为 &rect。
rect.Scale(2)
fmt.Printf("缩放后矩形: %+v, 面积: %.2f\n", rect, rect.Area())
// 指针类型也可以调用值接收者方法
rectPtr := &Rectangle{width: 3, height: 4}
// Go 自动将 rectPtr 解引用为 (*rectPtr) 来调用 Area()。
fmt.Printf("指针矩形: %+v, 面积: %.2f\n", *rectPtr, rectPtr.Area())
}
```
# 4. 结构体嵌套与组合
Go 语言通过结构体嵌套(或称嵌入)来实现**组合**,这是 Go `推荐`的代替传统面向对象中“继承”的设计模式。
## 4.1 显式嵌套
字段名和字段类型都明确给出。
```go
type Address struct {
City string
Country string
}
type Employee struct {
Name string
HomeAddress Address // 显式嵌套
}
// 访问: emp.HomeAddress.City
```
## 4.2 匿名嵌套(嵌入)
只给出字段类型,字段名默认为类型名。这会触发“字段提升”,使嵌套结构体的字段可以直接通过外部结构体访问。
```go
// [代码规范化]:这是一个完整的、可运行的示例,展示了显式和匿名嵌套。
package main
import "fmt"
// Address 结构体
type Address struct {
City string
Country string
}
// Contact 结构体,将作为匿名嵌套字段
type Contact struct {
Phone string
Email string
}
// Employee 结构体,组合了 Address 和 Contact
type Employee struct {
Name string
Age int
Addr Address // 显式嵌套,有字段名 Addr
Contact // 匿名嵌套(嵌入),字段被提升
}
func main() {
fmt.Println("--- 结构体嵌套与组合示例 ---")
emp := Employee{
Name: "王工程师",
Age: 35,
Addr: Address{
City: "深圳",
Country: "中国",
},
Contact: Contact{
Phone: "13800138000",
Email: "wang@example.com",
},
}
// 访问显式嵌套字段
fmt.Printf("地址: %s, %s\n", emp.Addr.City, emp.Addr.Country)
// 访问匿名嵌套(提升后)的字段
// 可以直接访问,就像它们是 Employee 的字段一样
fmt.Printf("联系方式: 电话 - %s, 邮箱 - %s\n", emp.Phone, emp.Email)
// 也可以通过类型名访问原始的匿名嵌套结构体
fmt.Printf("通过类型名访问邮箱: %s\n", emp.Contact.Email)
}
```
# 5. 匿名结构体 (Anonymous Structs)
匿名结构体是一种没有显式定义名称的结构体,非常适合用于那些只需要临时使用一次的数据结构,例如函数参数、返回值或测试数据。
```go
// [代码规范化]:这是一个完整的、可运行的示例,整合了匿名结构体的多种应用场景。
package main
import "fmt"
// getUserInfo 使用匿名结构体作为返回值,封装多个相关值。
func getUserInfo() struct {
ID int
Name string
} {
return struct {
ID int
Name string
}{
ID: 101,
Name: "临时用户",
}
}
func main() {
fmt.Println("--- 匿名结构体应用场景 ---")
// 场景1: 临时数据存储
// 在函数内部定义一个临时的、一次性的数据结构。
config := struct {
Host string
Port int
Enabled bool
}{
Host: "localhost",
Port: 8080,
Enabled: true,
}
fmt.Printf("临时配置: %+v\n\n", config)
// 场景2: 匿名结构体切片,常用于表格驱动测试
tests := []struct {
input int
expected string
}{
{input: 1, expected: "奇数"},
{input: 2, expected: "偶数"},
{input: -1, expected: "奇数"},
}
fmt.Println("表格驱动测试数据:")
for _, tc := range tests {
// 模拟测试逻辑
result := "偶数"
if tc.input%2 != 0 {
result = "奇数"
}
fmt.Printf(" 输入: %d, 期望: %s, 结果: %s, 通过: %t\n",
tc.input, tc.expected, result, result == tc.expected)
}
fmt.Println()
// 场景3: 作为函数返回值
user := getUserInfo()
fmt.Printf("从函数获取的用户信息: ID=%d, Name=%s\n", user.ID, user.Name)
}
```
# 6. 结构体字段的可见性 (Visibility)
Go语言使用大小写规则来控制可见性(导出性),而非 `public``private` 关键字。
- **大写开头**:导出的(Public)。结构体名或字段名如果以大写字母开头,则可以被包外的代码访问。
- **小写开头**:未导出的(Private)。结构体名或字段名如果以小写字母开头,则只能在定义它的包内部访问。
这个规则同样适用于函数、方法、常量和变量。
```go
// [代码规范化]:这是一个完整的、可运行的示例,用于解释可见性规则。
// 注意:真正的包外访问需要在不同的目录和文件中演示。
// 此处我们通过注释来解释这一概念。
package main
import "fmt"
// mypkg/models.go (模拟在一个名为 mypkg 的包中)
// ----------------------------------------------------
// User 是一个导出的结构体,因为它以大写字母'U'开头。
type User struct {
// ID 是一个导出的字段,因为'I'是大写的。包外代码可以访问 user.ID。
ID int
// email 是一个未导出的字段,因为'e'是小写的。包外代码无法访问 user.email。
email string
}
// NewUser 是一个导出的构造函数,用于创建 User 实例。
// 这是封装内部字段(如 email)的常用模式。
func NewUser(id int, email string) *User {
// 在包内部,我们可以访问所有字段,包括小写的 email。
return &User{
ID: id,
email: email,
}
}
// Email 方法是导出的,它提供了一种间接访问内部字段 email 的方式。
func (u *User) Email() string {
return u.email
}
// main.go (模拟在另一个包中)
// ----------------------------------------------------
func main() {
fmt.Println("--- 结构体可见性示例 ---")
// 我们可以调用 NewUser,因为它被导出了。
user := NewUser(1, "test@example.com")
// 我们可以直接访问导出的字段 ID。
fmt.Printf("用户ID (可访问): %d\n", user.ID)
// 以下代码如果位于不同的包中,将会导致编译错误:
// fmt.Println(user.email) // Error: user.email is not exported
// 我们只能通过导出的方法来访问它。
fmt.Printf("用户邮箱 (通过方法访问): %s\n", user.Email())
}
```
# 7. 结构体标签 (Struct Tags)
结构体标签是附加到结构体字段上的元数据字符串,在运行时可以通过反射读取。它们是 Go 语言实现与外部系统(如 JSON、XML、数据库 ORM)交互的关键机制。
标签的格式为:`\`` `key1:"value1" key2:"value2"` `` `
```go
// [代码规范化]:这是一个完整的、可运行的示例,展示了 JSON 标签的用法。
package main
import (
"encoding/json"
"fmt"
)
// Profile 定义了用户配置信息
type Profile struct {
// `json:"username"`: 定义了此字段在 JSON 中的键名。
Username string `json:"username"`
// `json:"-"`: 表示在 JSON 序列化/反序列化时忽略此字段。
Password string `json:"-"`
// `json:"age,omitempty"`: omitempty 表示如果字段值为零值(int 的 0),则在序列化时省略该字段。
Age int `json:"age,omitempty"`
// `json:"email"`: 正常映射。
Email string `json:"email"`
}
func main() {
fmt.Println("--- 结构体标签与 JSON 序列化示例 ---")
// 创建一个 Profile 实例
p1 := Profile{
Username: "gopher",
Password: "secret-password", // 这个字段将被忽略
Age: 0, // 这个字段因为 omitempty 和零值,将被省略
Email: "gopher@golang.org",
}
// 将结构体序列化为 JSON
jsonData, err := json.MarshalIndent(p1, "", " ") // MarshalIndent 用于格式化输出
if err != nil {
fmt.Println("JSON 序列化失败:", err)
return
}
// 输出结果,注意 Password 和 Age 字段都没有出现
fmt.Println("序列化后的 JSON:")
fmt.Println(string(jsonData))
}
```
# 8. 结构体与内存布局
Go 编译器会为了性能对结构体字段进行**内存对齐**。这意味着字段在内存中的顺序可能与你定义的顺序不同,并且可能会插入填充(padding)字节,以确保每个字段都从一个能被其大小整除的地址开始。
这对于需要与 C 库交互或进行底层性能优化的场景非常重要。
```go
// [代码规范化]:这是一个完整的、可运行的示例,演示了内存对齐的影响。
package main
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println("--- 结构体内存布局示例 ---")
// 在 64 位系统上:
// bool (1字节), float64 (8字节), int16 (2字节)
type S1 struct {
b bool // 1 字节
f float64 // 8 字节
i int16 // 2 字节
}
// 内存布局可能像这样:
// b (1 byte) | padding (7 bytes) | f (8 bytes) | i (2 bytes) | padding (6 bytes)
// 总大小会为了对齐到最大字段(float64, 8字节)的倍数而变为 24 字节。
s1 := S1{}
fmt.Printf("S1 - 非优化布局: 大小 = %d, 对齐 = %d\n", unsafe.Sizeof(s1), unsafe.Alignof(s1))
// 通过重新排序字段,可以减少内存占用
type S2 struct {
f float64 // 8 字节
i int16 // 2 字节
b bool // 1 字节
}
// 内存布局可能像这样:
// f (8 bytes) | i (2 bytes) | b (1 byte) | padding (5 bytes)
// 总大小会为了对齐到最大字段(float64, 8字节)的倍数而变为 16 字节。
s2 := S2{}
fmt.Printf("S2 - 优化后布局: 大小 = %d, 对齐 = %d\n", unsafe.Sizeof(s2), unsafe.Alignof(s2))
fmt.Println("\n结论:合理安排字段顺序可以节省内存空间。")
}
```