跳到主要内容

Pointer(指针)

指针保存某个变量的内存地址。Go 使用指针让函数访问同一份可变数据,或避免复制较大的值;普通 Go 代码不支持指针算术,内存仍由运行时管理。

取地址与解引用

&value 取得变量地址,结果类型为 *T*pointer 访问该地址指向的值:

package main

import "fmt"

func main() {
count := 3
pointer := &count

fmt.Println(pointer) // 一个内存地址
fmt.Println(*pointer) // 3

*pointer = 4
fmt.Println(count) // 4
}

这里 pointercount 指向同一个存储位置。修改 *pointer 就是在修改 count

指针的零值

指针的零值是 nil,表示它没有指向有效对象:

var pointer *int
fmt.Println(pointer == nil) // true

nil 指针解引用会触发 panic:

fmt.Println(*pointer) // panic

接收指针的函数应通过契约明确 nil 是否有效。若 nil 没有业务含义,通常在对象创建边界保证非 nil,比让每一层都重复猜测更清晰。

使用 new 创建指针

内置 new(T) 为类型 T 的零值准备存储,并返回 *T

counter := new(int)
fmt.Println(*counter) // 0
*counter = 10

它与先声明变量再取地址表达的含义接近:

var value int
counter := &value

大多数结构体更适合使用带字段名的复合字面量,因为初始化意图更明确。

Go 始终按值传参

函数参数始终通过赋值传递。把普通值传给函数会复制该值;把指针传给函数也会复制指针值,但复制后的指针仍指向同一个对象。

package main

import "fmt"

func increment(value *int) {
(*value)++
}

func main() {
count := 5
increment(&count)
fmt.Println(count) // 6
}

++ 在 Go 中是语句,不是可以嵌入其他表达式的运算符。这里先解引用指针,再递增原变量。

函数不需要通过指针修改数据时,优先接收值通常更容易理解。是否用指针还要考虑数据大小、方法集、是否需要表达可选值以及对象所有权,不能只依据“指针更快”判断;性能差异需要基准与逃逸分析等证据。

返回局部变量的地址

Go 可以安全返回局部变量的地址:

func newCounter() *int {
value := 0
return &value
}

只要该值仍可访问,运行时就会保证其存储有效,不会产生悬空指针。变量最终位于栈还是堆属于编译器的逃逸分析与实现决策,不应作为程序正确性的前提。

可以使用下面的命令观察当前编译器的逃逸分析信息,但输出可能随 Go 版本和代码变化:

go build -gcflags="-m" ./...

指针作为方法接收者

方法使用指针接收者时,可以修改原值:

package main

import "fmt"

type Counter struct {
Value int
}

func (counter *Counter) Increment() {
counter.Value++
}

func main() {
counter := Counter{}
counter.Increment()
fmt.Println(counter.Value) // 1
}

当变量可寻址时,调用 counter.Increment() 会由编译器按需要取得地址,这只是调用语法的便利。值接收者和指针接收者的方法集不同,接口满足关系也可能因此变化。

同一类型的方法通常统一选择值接收者或指针接收者。需要修改接收者、接收者包含不应复制的同步原语,或复制成本明显时,通常选择指针接收者。

方法集、方法值和接口赋值规则见 Method(方法)Interface(接口)

指针与切片、映射

切片和映射的值本身已经包含对底层数据的引用信息。函数修改切片现有元素或映射条目时,通常不需要传递 *[]T*map[K]V

func resetFirst(values []int) {
if len(values) > 0 {
values[0] = 0
}
}

func addScore(scores map[string]int) {
scores["Ada"] = 100
}

如果函数需要替换调用方持有的切片值,例如改变其长度并让调用方观察到,惯用方式是返回新切片:

func addValue(values []int, value int) []int {
return append(values, value)
}

只有在 API 确实需要修改切片头部本身或表达额外状态时,才考虑指向切片或映射的指针。

指针比较

指针可以与 nil 或同类型指针比较。两个指针相等,表示它们都为 nil,或指向同一个变量;比较的不是所指向值的内容。

left := 10
right := 10
fmt.Println(&left == &right) // false

比较对象内容时,应比较解引用后的值或使用该类型定义的相等规则。

共享状态与并发

指针让多个位置访问同一份数据,但不会自动提供并发安全。多个 goroutine 通过指针读写同一变量时,仍需使用互斥锁、原子操作、通道或其他同步机制建立 happens-before 关系。可以用 go test -race 检测数据竞态。

unsafe.Pointer 的边界

unsafe.Pointer 可以绕过普通指针的类型规则,并支持底层内存操作,但错误使用可能破坏类型安全、垃圾回收假设和跨平台兼容性。业务代码不应为了普通数据转换引入 unsafe;只有在系统编程、运行时互操作等场景中,依据 unsafe 包文档并配套测试审慎使用。

与 TypeScript/JavaScript 的关键差异

TypeScript/JavaScript 没有可直接取地址和解引用的普通指针语法。对象赋值会共享对象身份,可辅助理解“多个变量观察同一份可变数据”,但它不等同于 Go 指针;Go 还明确区分值、地址、指针类型和方法集。

并发共享状态需要结合 Sync(同步原语) 建立明确的 happens-before 关系。

小结

  • & 取地址,* 解引用,指针零值为 nil
  • Go 始终按值传参;传递指针时,被复制的指针仍指向同一个对象。
  • 可以安全返回局部变量地址,栈或堆的位置属于实现决策。
  • 指针接收者可修改原值,并会影响类型的方法集。
  • 指针带来共享访问,但不会自动提供并发同步或清晰的所有权。