讲解

指针存的是「变量的内存地址」。&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。下一节学习给类型挂行为:方法。