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
}
这里 pointer 与 count 指向同一个存储位置。修改 *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 始终按值传参;传递指针时,被复制的指针仍指向同一个对象。
- 可以安全返回局部变量地址,栈或堆的位置属于实现决策。
- 指针接收者可修改原值,并会影响类型的方法集。
- 指针带来共享访问,但不会自动提供并发同步或清晰的所有权。