15 KiB
15 KiB
1. 结构体定义与初始化
结构体通过 type 和 struct 关键字定义。初始化实例有多种灵活的方式。
1.1 定义语法
type StructName struct {
FieldName1 FieldType1
FieldName2 FieldType2
// ...
}
1.2 初始化实例
以下是四种最常见的初始化结构体的方式,每种方式都有其适用的场景。
// [代码规范化]:这是一个完整的、可运行的示例,整合了所有初始化方式。
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 中,直接传递结构体会产生一次完整的内存拷贝。对于大型结构体,这会影响性能。使用结构体指针可以避免拷贝,并且允许函数直接修改原始结构体实例。
// [代码规范化]:这是一个完整的、可运行的示例,清晰地对比了值传递和指针传递。
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 编译器会自动进行值和指针的转换,调用方便 |
[补充知识点] 最佳实践:如何选择接收者类型?
- 首选指针接收者:
- 修改状态:当方法需要修改接收者的字段时,必须使用指针。
- 性能:为避免大结构体的复制开销,即使不修改状态,也推荐使用指针。
- 一致性:如果一个类型已经有了指针接收者的方法,那么其他方法也应保持一致,使用指针接收者,以避免混淆。
- 何时使用值接收者:
- 不可变性:当你想保证该方法不会修改接收者时。
- 小结构体:对于非常小的、不包含指针或 slice 等引用类型的结构体(如
type Point struct{X, Y int}),值接收者的拷贝开销可以忽略不计。 - 并发安全:创建副本可以天然地避免在并发环境下对共享数据的意外修改(但通常有更好的并发控制方法)。
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 显式嵌套
字段名和字段类型都明确给出。
type Address struct {
City string
Country string
}
type Employee struct {
Name string
HomeAddress Address // 显式嵌套
}
// 访问: emp.HomeAddress.City
4.2 匿名嵌套(嵌入)
只给出字段类型,字段名默认为类型名。这会触发“字段提升”,使嵌套结构体的字段可以直接通过外部结构体访问。
// [代码规范化]:这是一个完整的、可运行的示例,展示了显式和匿名嵌套。
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)
匿名结构体是一种没有显式定义名称的结构体,非常适合用于那些只需要临时使用一次的数据结构,例如函数参数、返回值或测试数据。
// [代码规范化]:这是一个完整的、可运行的示例,整合了匿名结构体的多种应用场景。
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)。结构体名或字段名如果以小写字母开头,则只能在定义它的包内部访问。
这个规则同样适用于函数、方法、常量和变量。
// [代码规范化]:这是一个完整的、可运行的示例,用于解释可见性规则。
// 注意:真正的包外访问需要在不同的目录和文件中演示。
// 此处我们通过注释来解释这一概念。
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"``
// [代码规范化]:这是一个完整的、可运行的示例,展示了 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 库交互或进行底层性能优化的场景非常重要。
// [代码规范化]:这是一个完整的、可运行的示例,演示了内存对齐的影响。
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结论:合理安排字段顺序可以节省内存空间。")
}