讲解
指针存的是「变量的内存地址」。&x 取变量 x 的地址,得到 *int 类型(读作 int 指针);p 顺着指针取出指向的值,叫解引用。Go 的指针比 C 的安全得多:没有指针运算(不能 p+1 跳到下一个地址),也有垃圾回收兜底,所以学习难度其实不高,核心就是「& 取地址、 取值」两个操作。
为什么需要指针?因为 Go 函数传参一律是「值拷贝」——函数拿到的是实参的副本,改副本不影响原件。想让函数修改调用方的变量,就把变量的地址传进去,参数类型写成指针类型,函数里通过 *p = 新值 改的就是原件。同理,结构体很大时传指针也避免了整份拷贝的开销。
nil 是指针的零值,表示「不指向任何东西」。对 nil 指针解引用会立即 panic,这是 Go 新手最常见的崩溃原因,防御方法很朴素:使用指针前确认它不是 nil。最后记住一个 Go 的便利设计:通过指针访问结构体字段不用写 (*p).Field,直接 p.Field,编译器自动解引用。
示例
取地址、解引用与交换函数:
package main
import "fmt"
func swap(a, b *int) {
*a, *b = *b, *a // 通过指针修改调用方的变量
}
func main() {
x, y := 1, 2
p := &x
fmt.Println("p 指向的值:", *p)
swap(&x, &y)
fmt.Println("交换后:", x, y) // 2 1
}
值传递与指针传递的对比:
package main
import "fmt"
type Counter struct {
N int
}
func addValue(c Counter) { c.N++ } // 改的是副本
func addPointer(c *Counter) { c.N++ } // 改的是本体
func main() {
c := Counter{}
addValue(c)
fmt.Println("值传递后:", c.N) // 0,没变化
addPointer(&c)
fmt.Println("指针传递后:", c.N) // 1
}
nil 指针的安全检查:
package main
import "fmt"
func setName(p *int, v int) {
if p == nil {
fmt.Println("指针为 nil,拒绝写入")
return
}
*p = v
}
func main() {
var p *int
setName(p, 42) // 安全地被拒绝
n := 0
setName(&n, 42)
fmt.Println("写入成功:", n)
}
常见坑
- 对 nil 指针解引用:var p *int; fmt.Println(*p) 运行时直接 panic。指针使用前判 nil,函数返回指针的文档里说明是否可能为 nil。
- 以为指针能提高一切性能:小类型(int、小结构体)拷贝本来就快,传指针反而多一次间接寻址,还可能阻碍编译器优化。指针的第一用途是「共享修改」,不是盲目优化。
- 返回局部变量的指针却不放心:在 C 里这是悬垂指针灾难;Go 里编译器会自动把这样的变量「逃逸」到堆上,安全合法。但滥用逃逸会增加 GC 压力。
- 试图做指针运算:p++ 或 p+1 在 Go 里不存在。需要操作连续内存用切片,Go 刻意禁止指针算术以换取内存安全。
- 混淆 * 在类型和表达式里的含义:*int 里的星号表示「int 指针类型」,*p 里的星号表示「取 p 指向的值」。同一个符号两种角色,初期最容易看晕。
小结
& 取地址、* 解引用;函数传参默认值拷贝,传指针才能改原件;nil 指针解引用会 panic。下一节学习给类型挂行为:方法。