Go 指针、方法与值语义

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

Go 有指针,但没有指针运算:不能 p++ 遍历内存。它只有一个用途——让你决定「传副本」还是「传地址」。

取地址与解引用

x := 1
p := &x          // 取地址,p 的类型是 *int
fmt.Println(*p)  // 1,解引用取值

*p = 2           // 通过指针修改
fmt.Println(x)   // 2

零值方面,指针的零值是 nil:

var p *int
fmt.Println(p == nil)   // true
fmt.Println(*p)         // panic: nil pointer dereference

new(T) 分配一个类型为 T 的零值并返回指针,等价于先声明变量再取地址:

p := new(int)      // *int,指向 0
p2 := &struct{}{}  // 等价写法

日常更常用 &T{...},可同时初始化字段:

type User struct{ Name string }

u := &User{Name: "ann"}

指针作为参数

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

x := 1
increment(&x)
fmt.Println(x)   // 2

值传参是复制,指针传参才能修改调用方的变量。但 Go 里更常见的做法是返回值:

func increment(n int) int {
    return n + 1
}

修改入参会让函数有副作用,除非是明确的「更新」语义(如 json.Unmarshal(&cfg)),否则优先返回值。

值接收者与指针接收者

type Counter struct {
    n int
}

func (c Counter) IncByValue() {
    c.n++          // 改的是副本,无效
}

func (c *Counter) Inc() {
    c.n++          // 改的是原对象,有效
}
c := Counter{}
c.Inc()
c.IncByValue()
fmt.Println(c.n)   // 1,IncByValue 没有生效

调用时 Go 会自动取地址:c.Inc() 等价于 (&c).Inc(),前提是 c 可寻址(变量、结构体字段、切片元素都是)。

不可寻址的值无法调用指针方法:

map[string]Counter{"a": {}}["a"].Inc()   // 编译错误:不可寻址

// 正确:先取出、修改、再写回
c := m["a"]
c.Inc()
m["a"] = c

或者 map 的值直接存指针:map[string]*Counter。

方法集规则

简单记:指针类型的方法集包含值接收者 + 指针接收者的方法;值类型只包含值接收者。这影响接口实现:

type Incrementer interface {
    Inc()
}

var _ Incrementer = &Counter{}   // 可以:*Counter 有 Inc
var _ Incrementer = Counter{}    // 编译错误:Counter 没有 Inc 方法

所以当类型需要满足某个接口、而方法用了指针接收者时,要传 &T{} 而不是 T{}。

nil 接收者

方法可以对 nil 接收者做安全处理,这是常见技巧:

type Node struct {
    Val  int
    Next *Node
}

func (n *Node) Len() int {
    if n == nil {      // nil 也可以调用方法
        return 0
    }
    return 1 + n.Next.Len()
}

但访问 nil 指针的字段一定 panic,判断必须在访问前做。注意「接口里装着 nil 指针」不等于 nil 接口,详见下一篇。

何时该用指针

场景 用指针
需要修改接收者/入参 ✅ 必须
结构体较大(含多个字段或大数组),避免复制开销 ✅ 推荐
需要表达「可能不存在」 ✅ *T 的 nil 有语义
同一类型的方法接收者需保持一致 ✅ 统一用指针
只是读几个字段的小结构体 ❌ 值即可
slice、map、channel、func ❌ 本身含指针,传值即可

最后一条很实用:[]byte 传参不会复制底层数组,不需要写 *[]byte。

逃逸分析:不用太操心

局部变量的地址被返回到函数外时,编译器会把它分配到堆上(逃逸),这是自动的:

func NewUser(name string) *User {
    return &User{Name: name}   // 合法,Go 自动堆分配
}

不需要担心的点:这不是 C 的「返回局部变量地址」,编译器已经处理好。真正需要关注的是别为了性能过早优化,先用 go build -gcflags="-m" 看逃逸分析结果,再决定。

常见错误与注意事项

  1. 对 nil 指针取值:*p 或 p.Field 直接 panic,先判断 p != nil。
  2. 值接收者里改字段:改的是副本,方法「看起来没生效」。
  3. 接口用值类型实现指针方法:var _ I = T{} 编译报错,要传 &T{}。
  4. 对 map 元素调用指针方法:不可寻址,先取出再写回。
  5. 用指针无脑替换所有值传参:小结构体传值更快、更安全,也避免 nil 判断。
  6. 把指针当可变性开关:Go 里应通过返回值表达变化,指针只用于明确的原地更新。

小结

  • & 取地址、* 解引用,Go 指针不支持算术运算。
  • 值传参复制,指针传参可修改调用方。
  • 指针接收者能改字段,值接收者不能;方法集决定接口实现。
  • 方法是可寻址前提才能自动取地址,map 元素不可寻址。
  • 大结构体、需修改、需可空时用指针;slice / map 传值即可。

下一步