Day07(指针和结构体详解)
Go语言指针操作、结构体定义与方法、内存管理最佳实践
Day07(指针和结构体详解)
概述
指针和结构体是 Go 语言的核心概念,理解它们对于掌握 Go 的内存模型和面向对象编程至关重要。
目录
什么是指针?
指针是一个变量,存储的是另一个变量的内存地址。通过指针,我们可以间接访问和修改该变量的值。
指针运算符
&: 取地址运算符,获取变量的内存地址*: 解引用运算符,获取指针指向的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"
func main() {
// 声明普通变量
x := 42
// 获取指针
ptr := &x
fmt.Printf("x 的值: %d\n", x) // 42
fmt.Printf("x 的地址: %p\n", &x) // 0xc000...
fmt.Printf("ptr 的值(地址): %p\n", ptr) // 0xc000... (与 &x 相同)
fmt.Printf("ptr 指向的值: %d\n", *ptr) // 42 (解引用)
// 通过指针修改变量
*ptr = 100
fmt.Printf("修改后 x 的值: %d\n", x) // 100
}
指针声明方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
func main() {
// 方式 1: new() 函数 - 分配内存并返回指针
intPtr := new(int)
fmt.Printf("new(int): %p, value: %d\n", intPtr, *intPtr) // 0x..., 0
// 方式 2: 声明指针变量
var strPtr *string
fmt.Printf("未初始化的指针: %v\n", strPtr) // <nil>
// 方式 3: 取地址
name := "Go"
namePtr := &name
fmt.Printf("namePtr: %p, value: %s\n", namePtr, *namePtr)
}
指针的高级用法
指针与函数参数
Go 语言中,函数参数默认是值传递。如果需要修改原变量或避免大对象拷贝,应使用指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main
import "fmt"
// 值传递 - 不会修改原变量
func modifyByValue(x int) {
x = 100
}
// 指针传递 - 会修改原变量
func modifyByPointer(x *int) {
*x = 100
}
func main() {
a := 42
modifyByValue(a)
fmt.Printf("值传递后: %d\n", a) // 42 (未改变)
modifyByPointer(&a)
fmt.Printf("指针传递后: %d\n", a) // 100 (已改变)
}
nil 指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main
import "fmt"
func main() {
var ptr *int // 默认值为 nil
if ptr == nil {
fmt.Println("ptr is nil")
}
// ❌ 危险:解引用 nil 指针会导致 panic
// fmt.Println(*ptr) // panic: runtime error
}
结构体基础
什么是结构体?
结构体是用户自定义的复合类型,可以将不同类型的数据组合在一起。它是 Go 语言实现面向对象编程的基础。
结构体定义与初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main
import "fmt"
// User 用户结构体
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Active bool `json:"active"`
}
func main() {
// 方式 1: 声明零值结构体
var user1 User
fmt.Printf("零值结构体: %+v\n", user1)
// {Name: Age:0 Email: Active:false}
// 方式 2: 按字段顺序初始化
user2 := User{"Alice", 25, "alice@example.com", true}
fmt.Printf("按顺序: %+v\n", user2)
// 方式 3: 指定字段名初始化(推荐)
user3 := User{
Name: "Bob",
Age: 30,
Email: "bob@example.com",
Active: false,
}
fmt.Printf("指定字段: %+v\n", user3)
// 方式 4: new() 创建指针
user4 := new(User)
user4.Name = "Charlie"
user4.Age = 35
fmt.Printf("new(): %+v\n", *user4)
// 访问字段
fmt.Printf("Name: %s, Age: %d\n", user3.Name, user3.Age)
}
进阶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main
import "github.com/Cc360428/HelpPackage/UtilsHelp/logs"
// User 用户结构体
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
Statuc bool `json:"statuc"`
}
### 结构体方法
Go 语言通过**方法**为结构体添加行为。方法是带有接收者的函数。
#### 值接收者 vs 指针接收者
```go
package main
import "fmt"
type Counter struct {
Count int
}
// 值接收者 - 不会修改原结构体
func (c Counter) IncrementByValue() {
c.Count++
}
// 指针接收者 - 会修改原结构体(推荐)
func (c *Counter) IncrementByPointer() {
c.Count++
}
func main() {
c1 := Counter{0}
c1.IncrementByValue()
fmt.Printf("值接收者: %d\n", c1.Count) // 0 (未改变)
c2 := Counter{0}
c2.IncrementByPointer()
fmt.Printf("指针接收者: %d\n", c2.Count) // 1 (已改变)
}
最佳实践:
- 如果需要修改结构体,使用指针接收者
- 如果结构体较大,使用指针接收者避免拷贝
- 保持一致性:同一类型的方法统一使用值或指针接收者
完整示例:用户管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main
import (
"fmt"
"time"
)
type User struct {
Name string
Age int
Email string
CreatedAt time.Time
}
// NewUser 构造函数
func NewUser(name string, age int, email string) *User {
return &User{
Name: name,
Age: age,
Email: email,
CreatedAt: time.Now(),
}
}
// Greet 打招呼
func (u *User) Greet() string {
return fmt.Sprintf("Hello, I'm %s, age %d", u.Name, u.Age)
}
// IsAdult 判断是否成年
func (u *User) IsAdult() bool {
return u.Age >= 18
}
// UpdateEmail 更新邮箱
func (u *User) UpdateEmail(newEmail string) error {
if newEmail == "" {
return fmt.Errorf("email cannot be empty")
}
u.Email = newEmail
return nil
}
func main() {
user := NewUser("Alice", 25, "alice@example.com")
fmt.Println(user.Greet()) // Hello, I'm Alice, age 25
fmt.Printf("Is adult: %v\n", user.IsAdult()) // true
user.UpdateEmail("alice.new@example.com")
fmt.Printf("New email: %s\n", user.Email)
}
结构体标签(Struct Tags)
结构体标签是元数据,用于在序列化、ORM 等场景中提供额外信息。
JSON 标签示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main
import (
"encoding/json"
"fmt"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Category string `json:"category,omitempty"` // 为空时忽略
Secret string `json:"-"` // 完全忽略
}
func main() {
p := Product{
ID: 1,
Name: "Go Book",
Price: 29.99,
Category: "",
Secret: "password123",
}
// 序列化为 JSON
jsonData, _ := json.MarshalIndent(p, "", " ")
fmt.Println(string(jsonData))
/*
{
"id": 1,
"name": "Go Book",
"price": 29.99
}
*/
}
常见标签
| 库 | 标签示例 | 说明 |
|---|---|---|
| JSON | json:"name" |
JSON 字段名 |
| JSON | json:"name,omitempty" |
空值时忽略 |
| JSON | json:"-" |
忽略该字段 |
| BSON (MongoDB) | bson:"user_id" |
MongoDB 字段名 |
| GORM | gorm:"column:user_name" |
数据库列名 |
| Validate | validate:"required,email" |
验证规则 |
内存布局与对齐
结构体内存对齐
Go 编译器会对结构体字段进行内存对齐以提高访问效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main
import (
"fmt"
"unsafe"
)
type BadAlignment struct {
A bool // 1 byte
B int64 // 8 bytes (需要填充 7 bytes)
C bool // 1 byte (需要填充 7 bytes)
}
type GoodAlignment struct {
A int64 // 8 bytes
B bool // 1 byte
C bool // 1 byte
// 总大小:16 bytes (有 6 bytes 填充)
}
func main() {
fmt.Printf("BadAlignment size: %d bytes\n", unsafe.Sizeof(BadAlignment{}))
// 24 bytes (1 + 7(padding) + 8 + 1 + 7(padding))
fmt.Printf("GoodAlignment size: %d bytes\n", unsafe.Sizeof(GoodAlignment{}))
// 16 bytes (8 + 1 + 1 + 6(padding))
}
优化建议: 将大字段放在前面,小字段放在后面,可以减少内存占用
最佳实践总结
指针使用建议
✅ 应该使用指针的场景:
- 需要修改原变量
- 避免大结构体拷贝(> 几个 KB)
- 表示可选值(nil 表示不存在)
- 实现接口方法
❌ 不应该使用指针的场景:
- 基本类型(int, bool, string 等)
- 小型结构体(< 几个字段)
- 不需要修改且拷贝成本低
- 切片、Map、Channel(本身就是引用类型)
结构体设计建议
- 字段可见性: 仅导出需要公开的字段
- 构造函数: 提供
NewXxx()函数初始化 - 方法接收者: 保持一致性,优先使用指针接收者
- 标签使用: 为序列化字段添加合适的标签
- 零值可用: 设计使零值结构体也有意义
- 文档注释: 为导出的结构体和字段添加注释
常见陷阱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ❌ 错误:解引用 nil 指针
var ptr *int
fmt.Println(*ptr) // panic!
// ✅ 正确:检查 nil
if ptr != nil {
fmt.Println(*ptr)
}
// ❌ 错误:混淆值接收者和指针接收者
type Data struct{ Value int }
func (d Data) Modify() { d.Value = 10 } // 不会修改原值
// ✅ 正确:使用指针接收者
func (d *Data) Modify() { d.Value = 10 } // 会修改原值
小结
- 指针存储变量的内存地址,通过
&取地址,*解引用 - 结构体组合不同类型的数据,是 Go 的面向对象基础
- 方法通过接收者为结构体添加行为
- 指针接收者可以修改结构体,值接收者不能
- 结构体标签为序列化等场景提供元数据
- 合理的内存对齐可以提升性能
掌握指针和结构体是成为 Go 高手的关键一步!🎯
本文由作者按照
CC BY 4.0
进行授权