Go 语言指针赋值详解

Go 语言指针赋值详解

1. 基本概念

指针是什么?

指针是存储另一个变量内存地址的变量。在 Go 中,指针用 * 符号表示。

基本语法

go 复制代码
var ptr *int    // 声明一个指向 int 的指针
ptr = &value    // 将 value 的地址赋值给 ptr
*ptr = 100      // 通过指针修改值

2. 指针声明和初始化

2.1 声明指针

go 复制代码
// 方式1: 先声明,后赋值
var ptr *int
var value int = 42
ptr = &value

// 方式2: 声明时初始化
ptr := &value

// 方式3: 使用 new 函数
ptr := new(int)
*ptr = 42

2.2 结构体指针

go 复制代码
type Post struct {
    ID    uint
    Title string
    View  int
}

// 方式1: 先创建结构体,再取地址
post := Post{ID: 1, Title: "文章1", View: 100}
postPtr := &post

// 方式2: 直接创建结构体指针
postPtr := &Post{ID: 1, Title: "文章1", View: 100}

// 方式3: 使用 new 函数
postPtr := new(Post)
postPtr.ID = 1
postPtr.Title = "文章1"
postPtr.View = 100

3. 指针赋值操作

3.1 基本赋值

go 复制代码
var num int = 42
ptr1 := &num
ptr2 := ptr1  // ptr2 现在指向与 ptr1 相同的内存地址

// 修改其中一个指针指向的值,另一个也会改变
*ptr1 = 100
fmt.Println(*ptr2) // 输出: 100

3.2 切片中的指针赋值

go 复制代码
// 指针切片
posts := []*Post{
    {ID: 1, Title: "文章1", View: 10},
    {ID: 2, Title: "文章2", View: 20},
}

// 获取切片中元素的指针
firstPostPtr := posts[0]
firstPostPtr.Title = "修改后的文章1"  // 这会修改原始数据

3.3 函数参数中的指针

go 复制代码
func modifyPost(post *Post) {
    post.Title = "修改后的标题"  // 修改会影响原始数据
}

post := Post{ID: 1, Title: "原始标题", View: 0}
modifyPost(&post)  // 传递地址
fmt.Println(post.Title) // 输出: "修改后的标题"

4. 指针的特殊情况

4.1 指针的零值

go 复制代码
var ptr *Post  // ptr 的零值是 nil
fmt.Println(ptr) // 输出: <nil>

// 安全的 nil 指针检查
if ptr != nil {
    fmt.Println(ptr.Title)
} else {
    fmt.Println("指针是 nil")
}

4.2 返回指针

go 复制代码
func createPost(id uint, title string, view int) *Post {
    post := Post{ID: id, Title: title, View: view}
    return &post  // Go 编译器会优化,不会返回栈上变量的地址
}

postPtr := createPost(1, "新文章", 100)

4.3 多重指针

go 复制代码
value := 42
ptr := &value
ptrToPtr := &ptr

fmt.Printf("value = %d\n", value)           // 42
fmt.Printf("*ptr = %d\n", *ptr)             // 42
fmt.Printf("**ptrToPtr = %d\n", **ptrToPtr) // 42

5. 指针数组和数组指针

5.1 指针数组

go 复制代码
// 指针数组:数组中的每个元素都是指针
numbers := []int{1, 2, 3}
pointerArray := [3]*int{&numbers[0], &numbers[1], &numbers[2]}

// 修改指针指向的值
*pointerArray[0] = 100
fmt.Println(numbers[0]) // 输出: 100

5.2 数组指针

go 复制代码
// 数组指针:指向整个数组的指针
numbers := [3]int{1, 2, 3}
arrayPointer := &numbers

// 通过数组指针访问元素
fmt.Println(arrayPointer[0]) // 输出: 1
fmt.Println((*arrayPointer)[0]) // 等价写法

6. 常见陷阱和注意事项

6.1 循环中的指针问题

go 复制代码
// ❌ 错误的方式:所有指针都指向同一个变量
numbers := []int{1, 2, 3}
var pointers []*int
for _, num := range numbers {
    pointers = append(pointers, &num) // 错误!所有指针都指向循环变量
}

// ✅ 正确的方式:为每个值创建独立的指针
var correctPointers []*int
for i := range numbers {
    correctPointers = append(correctPointers, &numbers[i])
}

6.2 nil 指针解引用

go 复制代码
var ptr *Post
// fmt.Println(ptr.Title) // 这会导致 panic

// 安全的访问方式
if ptr != nil {
    fmt.Println(ptr.Title)
}

6.3 返回局部变量地址

go 复制代码
// 概念上不推荐,但 Go 编译器会优化
func badPractice() *Post {
    post := Post{ID: 1, Title: "局部变量", View: 100}
    return &post // Go 编译器会优化,不会返回栈上变量的地址
}

7. 实际应用场景

7.1 数据库操作(如项目中的使用)

go 复制代码
func GetPostById(id uint) (*Post, error) {
    var post Post
    err := DB.First(&post, "id = ?", id).Error
    return &post, err  // 返回指针,避免大型结构体的复制
}

7.2 函数参数传递

go 复制代码
// 当需要修改原始数据时使用指针
func UpdatePostView(post *Post) {
    post.View++
}

// 当不需要修改原始数据时使用值
func GetPostExcerpt(post Post) string {
    return post.Title[:10] + "..."
}

7.3 切片和映射中的指针

go 复制代码
// 指针切片:共享数据
posts := []*Post{
    {ID: 1, Title: "文章1", View: 10},
    {ID: 2, Title: "文章2", View: 20},
}

// 修改会影响原始数据
posts[0].Title = "修改后的文章1"

// 值切片:独立副本
postValues := []Post{
    {ID: 1, Title: "文章1", View: 10},
    {ID: 2, Title: "文章2", View: 20},
}

// 修改不会影响原始数据
postValues[0].Title = "修改后的文章1"

8. 性能考虑

8.1 何时使用指针

  • 大型结构体:避免复制开销
  • 需要修改原始数据:函数内部修改需要影响调用者
  • 接口实现:某些接口要求指针接收者

8.2 何时使用值

  • 小型结构体:复制开销小
  • 不需要修改原始数据:函数式编程风格
  • 需要数据隔离:确保数据不被意外修改

9. 最佳实践

  1. 一致性:在项目中保持一致的指针使用风格
  2. 明确意图:使用指针明确表示需要修改数据
  3. nil 检查:始终检查指针是否为 nil
  4. 避免过度使用:不要为了使用指针而使用指针
  5. 文档化:在函数文档中说明参数和返回值的指针语义

10. 总结

Go 语言中的指针赋值是一个强大的特性,但需要谨慎使用:

  • 指针赋值:两个指针指向同一块内存
  • 值赋值:创建数据的独立副本
  • 选择原则:根据数据大小、修改需求和性能要求选择
  • 安全第一:始终进行 nil 指针检查
  • 保持一致性:在项目中遵循统一的指针使用规范
相关推荐
研究司马懿1 天前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大2 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰2 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘2 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤2 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto4 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto6 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室7 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题7 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo