Menu

Go 指针:& 与 *、new、nil 以及何时使用指针

指针保存的是一个值的地址。学习 & 和 *、new、结构体指针、为什么可以安全地返回局部变量的指针、什么时候该用指针,以及空指针解引用引发的 panic。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

& 和 *

指针保存的是一个值的内存地址。&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 来处理连续的内存。

Coddy 编程语言插图

和 Coddy 一起学编程

开始