文章

Day07(指针和结构体详解)

Go语言指针操作、结构体定义与方法、内存管理最佳实践

Day07(指针和结构体详解)

概述

指针和结构体是 Go 语言的核心概念,理解它们对于掌握 Go 的内存模型和面向对象编程至关重要。

目录

  1. 指针基础
  2. 指针的高级用法
  3. 结构体基础
  4. 结构体方法
  5. 结构体标签
  6. 内存布局与对齐
  7. 最佳实践

    指针基础

什么是指针?

指针是一个变量,存储的是另一个变量的内存地址。通过指针,我们可以间接访问和修改该变量的值。

指针运算符

  • & : 取地址运算符,获取变量的内存地址
  • * : 解引用运算符,获取指针指向的值
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))
}

优化建议: 将大字段放在前面,小字段放在后面,可以减少内存占用

最佳实践总结

指针使用建议

应该使用指针的场景:

  1. 需要修改原变量
  2. 避免大结构体拷贝(> 几个 KB)
  3. 表示可选值(nil 表示不存在)
  4. 实现接口方法

不应该使用指针的场景:

  1. 基本类型(int, bool, string 等)
  2. 小型结构体(< 几个字段)
  3. 不需要修改且拷贝成本低
  4. 切片、Map、Channel(本身就是引用类型)

结构体设计建议

  1. 字段可见性: 仅导出需要公开的字段
  2. 构造函数: 提供 NewXxx() 函数初始化
  3. 方法接收者: 保持一致性,优先使用指针接收者
  4. 标签使用: 为序列化字段添加合适的标签
  5. 零值可用: 设计使零值结构体也有意义
  6. 文档注释: 为导出的结构体和字段添加注释

常见陷阱

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 进行授权