490 lines
15 KiB
Markdown
490 lines
15 KiB
Markdown
# 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结论:合理安排字段顺序可以节省内存空间。")
|
||
}
|
||
```
|