Go 指针、方法与值语义
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
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" 看逃逸分析结果,再决定。
常见错误与注意事项
- 对 nil 指针取值:
*p或p.Field直接 panic,先判断p != nil。 - 值接收者里改字段:改的是副本,方法「看起来没生效」。
- 接口用值类型实现指针方法:
var _ I = T{}编译报错,要传&T{}。 - 对 map 元素调用指针方法:不可寻址,先取出再写回。
- 用指针无脑替换所有值传参:小结构体传值更快、更安全,也避免 nil 判断。
- 把指针当可变性开关:Go 里应通过返回值表达变化,指针只用于明确的原地更新。
小结
&取地址、*解引用,Go 指针不支持算术运算。- 值传参复制,指针传参可修改调用方。
- 指针接收者能改字段,值接收者不能;方法集决定接口实现。
- 方法是可寻址前提才能自动取地址,map 元素不可寻址。
- 大结构体、需修改、需可空时用指针;slice / map 传值即可。