Go 指针入门
指针(Pointers)是 Go 语言中一个极为硬核且基础的概念,它允许你直接与计算机的内存地址打交道。
熟练掌握指针,是你写出高性能、低内存占用 Go 代码的必经之路。特别是在处理复杂数据结构(如大型 Struct)、函数参数传递以及底层内存管理时,指针的作用无可替代。
本章将带你从零开始,搞懂如何声明指针、如何通过解引用来读取和修改真实数据,以及指针与普通变量之间那种“千丝万缕”的联系。
1. 什么是指针?
指针也是一种变量,只不过它存储的不是普通的数值(比如 10 或者 "Hello"),而是另一个变量的“内存地址”。
打个通俗的比方:如果把计算机内存看作一条长长的街道,普通的变量就像是街道上的一栋栋房子,里面装着具体的人(数据)。而指针,就像是写着某栋房子具体门牌号的纸条。指针本身不装人,它只告诉你:“顺着这个门牌号找过去,你就能找到那个人”。
1.1 声明指针
在 Go 语言中声明指针,你需要使用星号 * 符号,紧跟在它将要指向的数据类型前面。基本语法如下:
var 指针名 *数据类型例如,要声明一个专门用来存放整数 (int) 内存地址的指针,你可以这样写:
var ptr *int这行代码声明了一个名为 ptr 的变量。需要牢记的是,ptr 自己也要占用一点点内存空间,而它里面装的内容,将是一个内存地址。
1.2 基础示例
来看一个完整的例子,感受一下指针是如何和普通变量绑定的:
package main
import "fmt"
func main() {
var num int = 42
var ptr *int
// 把变量 'num' 的内存地址提取出来,赋值给 'ptr'
ptr = &num
fmt.Println("num 的内存地址:", &num) // 输出类似: 0xc0000160b8 (每次运行可能不同)
fmt.Println("ptr 里面存的值:", ptr) // 输出类似: 0xc0000160b8 (与上面的地址完全一样!)
}在这个例子中:
num是一个普通整数,值为 42。ptr是一个指针变量。&num使用了取址符&,它的作用是揪出num变量在内存中的真实物理地址。ptr = &num就是把这张写着num地址的“门牌号纸条”交给了ptr保存。
2. 指针的零值与防雷指南
和 Go 语言中的其他类型一样,指针也有自己的默认“零值”。指针的零值是 nil。
一个 nil 指针意味着这张“门牌号纸条”是空白的,它没有指向任何现实存在的内存地址。这是一个极其危险的雷区:如果你试图强行访问(解引用)一个 nil 指针指向的数据,程序会直接崩溃 (Panic)!
package main
import "fmt"
func main() {
var ptr *int // 声明了,但没给它任何地址,所以它是 nil
fmt.Println("ptr 的值:", ptr) // 输出: <nil>
fmt.Println("ptr == nil:", ptr == nil) // 输出: true
// 🚨 致命错误演示:取消下面这行的注释会导致程序崩溃
// fmt.Println(*ptr) // Panic: invalid memory address or nil pointer dereference
}黄金法则: 在顺着指针去拿数据之前,一定要确保它不是空的(if ptr != nil)。
3. 取址符 (&):获取内存地址
如前所述,取址符 & 是我们连接普通变量和指针的桥梁。把它放在任何一个普通的变量前面,你就能得到它在内存中的安身之所。
package main
import "fmt"
func main() {
name := "Alice"
address := &name // 提取 'name' 变量的内存地址
fmt.Println("name 的值:", name) // 输出: Alice
fmt.Println("name 的地址:", &name) // 输出类似: 0xc000010230
fmt.Println("address 变量的值:", address) // 输出类似: 0xc000010230
}4. 解引用 (*):通过地址读写数据
既然我们有了地址,下一步自然是顺着地址去找数据了。这个过程被称为解引用 (Dereferencing)。
在指针变量的前面加上星号 *,就等于下达了指令:“Go,顺着这个地址找过去,把里面的数据给我读出来,或者让我改掉它!”
4.1 读取真实数据
package main
import "fmt"
func main() {
num := 100
ptr := &num // 'ptr' 拿到了 'num' 的地址
fmt.Println("num 本身的值:", num) // 输出: 100
fmt.Println("ptr 存的地址:", ptr) // 输出类似: 0xc000010248
// 使用 * 解引用:顺着 ptr 存的地址,抓取里面的数据
fmt.Println("ptr 指向的真实数据:", *ptr) // 输出: 100
}4.2 隔山打牛:通过指针修改原数据
这是指针最强大的威力所在。当你解引用一个指针并对其进行赋值时,你实际上是直接在修改那个原始变量的内存数据!
package main
import "fmt"
// modifyValue 函数接收一个整数的指针作为参数
func modifyValue(ptr *int) {
// 顺着指针找到原数据,将其翻倍,再存回那个内存地址
*ptr = *ptr * 2
}
func main() {
number := 50
fmt.Println("修改前的值:", number) // 输出: 50
// 注意:我们要传的是 number 的地址,所以要加 &
modifyValue(&number)
// 原来的 number 已经被隔空修改了!
fmt.Println("修改后的值:", number) // 输出: 100
}在这个例子中,因为我们把 number 的真实地址(&number)交给了 modifyValue 函数,函数内部通过 *ptr 直接操作了那块内存,从而成功地修改了外面的 number 变量。这打破了常规函数“传值拷贝”的隔离。
5. 指针与结构体 (Struct) 的梦幻联动
在实际开发中,指针最常用于和复杂的数据结构(尤其是结构体 Struct)配合使用。传递大型结构体的指针,不仅能让你修改里面的字段,还能避免在函数间传递时发生极其消耗性能的全量内存拷贝。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
person := Person{Name: "Bob", Age: 30}
ptr := &person // 拿到 person 结构体的指针
fmt.Println("原始的 Person:", person) // 输出: {Bob 30}
// 严格语法的解引用读取字段
fmt.Println("通过指针读取名字:", (*ptr).Name) // 输出: Bob
// 🌟 Go 语言的贴心语法糖:自动隐式解引用
// 你不需要写 (*ptr).Age = 35,直接写 ptr.Age 即可,Go 编译器会自动帮你解引用!
ptr.Age = 35
fmt.Println("修改后的 Person:", person) // 输出: {Bob 35}
}注意上面的代码:理论上你需要写 (*ptr).Name 来确保先解引用拿到结构体,再点出 Name 字段(因为 . 运算符的优先级高于 *)。但 Go 语言非常人性化,当通过结构体指针访问字段时,允许你直接简写为 ptr.Age,编译器会在底层自动帮你完成解引用。