Go 语言中指针介绍
指针是 Go 语言中一个非常核心的概念,也是许多从 PHP 等高级语言转过来的开发者最需要适应的一个点。不过别担心,Go 的指针比 C/C++ 中的指针要安全和简单得多。
理解指针的关键在于,它能让你在程序的不同部分共享和修改同一份数据,而不是传来传去的数据副本。
我们同样从简单到高阶,分场景来彻底搞懂它。
核心概念:什么是指针?
在探讨场景前,我们必须先建立一个清晰的心理模型。
想象一下,一个变量 v
就像一座房子 🏠,里面住着数据(比如数字 10
)。
- 变量
v
本身代表房子,可以直接访问里面的数据。 - 指针
p
则是写着这座房子地址的一张纸条 📝。
指针(Pointer)本身也是一个变量,但它存储的不是普通数据,而是另一个变量的内存地址。
Go 提供了两个核心操作符来使用指针:
&
(取地址符): 当用在变量前面时(如&v
),它的意思是“给我这座房子的地址”。*
(解引用符): 当用在指针变量前面时(如*p
),它的意思是“去纸条上写的地址,把那座房子里的东西取出来”。它也用在类型声明中,表示一个指针类型(如*int
表示一个指向整数的指针)。
场景一:基础且最常见的用法
1. 在函数中修改外部变量
这是你学习指针的首要理由,也是我们之前在“方法”部分遇到的指针接收者的原理。
- 场景: 你需要写一个函数,这个函数要能修改传入的参数的值,而不是修改一个副本。例如,一个函数需要将外部的一个计数器加一。
- 用法:
a) 错误示范 (值传递)
b) 正确示范 (指针传递)func addOne_Fail(num int) {num = num + 1 // 这里修改的是 num 的一个副本 }func main() {x := 5addOne_Fail(x)fmt.Println(x) // 输出仍然是 5,原始的 x 没有被改变 }
我们之前学的// 参数是一个指向 int 的指针 func addOne_Success(num *int) {// *num 的意思是“去 num 这个地址,把里面的值取出来”// 然后对这个值进行修改*num = *num + 1 }func main() {x := 5// &x 的意思是“把 x 的地址传进去”addOne_Success(&x)fmt.Println(x) // 输出是 6,原始的 x 被成功修改了! }
func (u *User) HaveBirthday()
就是这个原理。u
接收的是User
实例的地址,所以方法能修改原始的User
。
场景二:中阶/性能考量
2. 避免大数据结构的值拷贝
当你的结构体非常大时,在函数间以“值传递”的方式传来传去,会产生巨大的性能开销。因为每一次函数调用,Go 都需要完整地复制一遍整个结构体。
- 场景:
- 你有一个包含大量字段的配置结构体
Config
,需要把它传递给多个函数去读取其中的不同部分。 - 你有一个包含大数据块(比如一个图片或一段音频)的结构体。
- 你有一个包含大量字段的配置结构体
- 用法: 通过传递结构体的指针,你实际上只复制了一个非常小的内存地址(通常是 8 字节),而不是整个庞大的结构体数据。
经验法则:当你的结构体比较大,或者需要在多处共享和修改时,优先使用指针。type BigStruct struct {// 假设这里有很多字段,占用很大内存data [1024 * 10]byte // 10KB 的数据 }// 低效的方式:每次调用都会复制 10KB 的数据 func processByValue(s BigStruct) { /* ... */ }// 高效的方式:只复制一个 8 字节的指针 func processByPointer(s *BigStruct) { /* ... */ }func main() {data := BigStruct{}processByPointer(&data) // 这是更推荐的做法 }
场景三:高阶/地道用法
3. 表示“可选”或“缺失”的值
一个指针的零值是 nil
。这个特性非常有用,可以用来区分“一个值为零”和“根本没有提供值”这两种情况。
- 场景: 你在设计一个用于更新用户信息的 API。用户只想更新他的年龄,而不想改动姓名。如果你的更新结构体是
struct { Name string; Age int }
,那么传过来的 JSON{"age": 30}
会导致Name
字段是空字符串""
。你是要更新用户的姓名为空,还是用户根本没提供姓名?这就有歧义了。 - 用法: 将结构体字段定义为指针类型。如果 JSON 中没有这个字段,那么指针的值就是
nil
。
输出会是:type UpdateUserPayload struct {Name *string `json:"name,omitempty"`Age *int `json:"age,omitempty"` }func main() {// 假设只收到了 age 字段jsonStr := `{"age": 31}`var payload UpdateUserPayloadjson.Unmarshal([]byte(jsonStr), &payload)if payload.Name != nil {fmt.Println("用户想要更新姓名为:", *payload.Name)} else {fmt.Println("用户没有提供新的姓名。")}if payload.Age != nil {fmt.Println("用户想要更新年龄为:", *payload.Age)} else {fmt.Println("用户没有提供新的年龄。")} }
通过检查指针是否为用户没有提供新的姓名。 用户想要更新年龄为: 31
nil
,我们就能准确地知道用户到底想更新哪些字段。
4. 构建链式数据结构
像链表、树这样的数据结构,其节点之间就是通过指针连接起来的。
- 场景: 实现一个链表或二叉树。
- 用法: 结构体中包含一个或多个指向同类型结构体的指针。
nil
指针自然地表示链表的末尾或树的叶子节点。type Node struct {Value intNext *Node // 指向下一个节点的指针 }func main() {node1 := Node{Value: 10}node2 := Node{Value: 20}node1.Next = &node2 // node1 的 Next 指针指向 node2 的地址// 这样,node1 和 node2 就连接起来了fmt.Println(node1.Value) // 10fmt.Println(node1.Next.Value) // 通过指针访问下一个节点的值: 20 }
总结
对于有 PHP 背景的你来说,可以这样简单理解:
- 当你需要在函数或方法内部修改外部的变量时,使用指针。
- 当你传递一个很大的结构体,想避免复制开销时,使用指针。
- 当你需要表达一个值**“可能不存在”**时,使用指针。
Go 的指针是一个强大但清晰的工具,它让你能更精确地控制程序的内存和数据流。