& 和 *
指针保存的是一个值的内存地址。&x 得到指向 x 的指针。*p 顺着指针找到它指向的值。
星号有两种作用。在类型中(*int),它表示“指向 int 的指针”。在表达式中(*p),它表示“p 所指向的值”。直接打印 p 会显示类似 0xc000012345 的地址,每次运行都可能不同。
为什么需要指针:共享而不是复制
Go 中的一切都是值传递。函数拿到的是每个参数的副本,所以它无法修改调用方的变量。传递指针时复制的是地址,双方访问的就是同一个值。
Go 没有 C++ 意义上的引用。Go 中所说的“引用传递”,其实是按值传递一个指针。
结构体指针
结构体指针是 Go 代码中最常见的指针。访问字段时会自动解引用:p.Name 的意思是 (*p).Name。
方法也遵循同样的逻辑:指针接收者让方法可以修改结构体。方法页面讲解了值接收者和指针接收者该如何选择。
new
new(T) 分配一个零值的 T 并返回它的地址。这和先声明一个变量再取它的地址是一样的。
p := new(int) // *int pointing at 0
var x int
q := &x // same thing, two lines
a := new(Account) // same as &Account{}
实际上,对结构体更常用的是 &T{...},因为它可以在同一个表达式中设置字段。new 适合用来创建指向基本类型的指针。不要把它和 make 混淆,make 用来构建 slice、map 和 channel,返回的是可以直接使用的值,而不是指针。
返回局部变量的指针是安全的
在 C 中,返回局部变量的地址是一个 bug。在 Go 中这是很普通的代码。
编译器的逃逸分析决定一个值放在栈上还是堆上。你从来不需要选择,也从来不需要释放内存:当没有任何东西指向它时,垃圾回收器会回收它。如果你好奇,go build -gcflags=-m 会打印出这些决定。
nil 指针
任何指针类型的零值都是 nil。对 nil 解引用会引发 panic。
输出:
1
2
true
recovered: runtime error: invalid memory address or nil pointer dereference
如果没有 recover,程序会带着这条信息崩溃,调用栈以 panic: runtime error: invalid memory address or nil pointer dereference 和 [signal SIGSEGV: segmentation violation ...] 开头。常见的来源有:
- 一个从未赋值的指针变量或结构体字段
- 在
map[string]*T上用不存在的键执行m[key],返回的是nil - 忽略 error:
f, _ := os.Open(path)失败时f为 nil - 在 nil 指针上调用指针接收者的方法,而方法又读取了字段
在 nil 指针上调用方法本身是合法的。只有当方法访问接收者的字段时才会 panic。
什么时候用指针
以下情况使用指针:
- 函数或方法必须修改调用方的值
- 值是一个很大的结构体,并且经常被传来传去
- 这个类型不能被复制(它包含
sync.Mutex,或者代表一个唯一的资源,比如一个连接) - 你需要用
nil表示“没有值”,例如 JSON 结构体中的可选字段(*int能区分“不存在”和0)
以下情况不要用指针:
- 值很小而且只读(
time.Time、一个Point) - 类型本身就类似引用:slice、map、channel、函数和接口很少需要在前面加
*。*[]int或*map[string]int几乎总是写错了
指针并不会自动更快。复制一个小结构体的开销很小,而指针可能迫使值分配在堆上,并给垃圾回收器增加工作量。
指针与循环变量
从 Go 1.22 起,获取循环变量的地址是安全的,因为每次迭代都有自己的变量:
这段代码打印 0 1 2。在 Go 1.22(它同时引入了 range 3)之前,写成 for i := 0; i < 3; i++ 的同一个循环会打印 3 3 3,因为三个指针共享同一个变量。注意,for _, v := range items 中的 &v 指向的是元素的副本,而不是 slice 中的元素本身。要得到指向元素本身的指针,使用 &items[i]。
常见错误
- 不检查就解引用。 只要可能为 nil,就检查
if p == nil。 - 指向 range 值的指针。
&v指向的是副本;应使用&s[i]。 - 指向 slice 和 map 的指针。 很少需要。应该返回新的 slice。
- 在
append前后一直持有&s[i]。 如果append重新分配了内存,指针仍然指向旧数组,之后的写入就去了一个看不见的地方。
常见问题
Go 中的 & 和 * 是什么意思?
&x 获取 x 的地址,得到一个类型为 *T 的指针。*p 对指针解引用:读取或写入它所指向的值。在类型中,*T 表示“指向 T 的指针”。
Go 中 new 和 make 有什么区别?
new(T) 分配一个零值的 T 并返回 *T;它适用于任何类型。make 只适用于 slice、map 和 channel,返回的是该类型一个已初始化的值(不是零值,也不是指针):make(map[string]int) 可以直接使用,而 new(map[string]int) 是一个指向 nil map 的指针。
Go 中可以返回局部变量的指针吗?
可以。与 C 不同,这样做是安全的。编译器的逃逸分析会发现这个变量的生命周期超出了函数,于是把它分配在堆上。func newInt() *int { x := 5; return &x } 是正确的 Go 代码。
Go 中 "invalid memory address or nil pointer dereference" 是什么原因?
通过 nil 指针读取或写入:一个从未赋值的 *T 变量、在 map[K]*V 中查找不存在的键,或者一个函数返回了 nil 和一个你没有检查的 error。先检查是否为 nil,或者在使用前初始化指针。
Go 有指针运算吗?
没有。你不能对指针做加法,也不能通过它按索引访问内存。unsafe 包允许在底层代码中这样做,但不提供任何安全保证。普通的 Go 代码用 slice 来处理连续的内存。