很多人学 Go 指针,只会记住两句话:
&取地址*解引用
然后代码能跑,但心里其实是懵的:
- 取地址到底在干嘛?
- 解引用为什么能改原值?
- 为什么有时候必须用指针接收者?
- 为什么接口实现又跟指针有关?
这篇我们不讲抽象。只做一件事:把“取地址”和“解引用”彻底讲透,然后自然推到语义层。
🔥语法层:& 和 * 到底在干什么?
下方是内存结构图,后面我们会用到。
内存:
┌──────────────────┐
│ 0xc0000100a8 │
│------------------│
│ 25 │ ← age
└──────────────────┘
age → 0xc0000100a8
p → 0xc0000100a8
变量的本质:一块内存
在 Go 里,所有变量都存在内存中。
age := 25
可以理解为:
内存某个位置:存着 25
变量名 age:指向那块内存
变量名只是一个“标签”,真正存数据的是那块内存。
& 取地址 —— 拿到那块内存的位置
p := &age
&age 的意思是:
把 age 那块内存的位置拿出来。
此时关系变成:
age → 25
p → 存的是 age 的地址
注意:
- p 不是 25
- p 也不是 age
- p 是“指向 age 的地址”
* 解引用 —— 顺着地址找到值
fmt.Println(p)
fmt.Println(*p)
*p 的意思是:
顺着 p 这个地址,找到它指向的那块内存,把值取出来。
所以:
p是地址 => 0xc0000100a8*p是地址里的值 => 25
更关键的是:
*p = 30
这行代码的意思是:
顺着地址找到原来的变量,然后把那块内存改成 30。
所以:
age := 25
p := &age
*p = 30
内存变成:
┌────────────────────┐
│ 0xc0000100a8 │
│--------------------│
│ 30 │ ← 被修改
└────────────────────┘
结果:
age == 30
不是“指针很神奇”。而是:
你通过地址,直接改了那块内存。
🔥行为层 —— 拷贝 vs 共享
指针真正有意义,是在函数传参的时候。
默认是值拷贝
看这个:
func modify(x int) {
x = 100
}
这里发生了什么?
- 函数收到的是一份 拷贝
- 修改的是拷贝
- 原变量不受影响
因为 Go 默认是 值拷贝传递
传指针 = 共享同一块内存
再看指针版本:
func modify(x *int) {
*x = 100
}
调用:
age := 25
modify(&age)
过程是:
- 把 age 的地址传进去
- 函数里通过
*x找到原始内存 - 修改那块内存
本质区别:
| 传值 | 传指针 |
|---|---|
| 拷贝数据 | 共享同一块内存 |
| 改副本 | 改原件 |
这就是指针的第一层意义:
共享同一块内存。
那为什么结构体经常用指针?
值接收者:操作副本
type User struct {
Name string
Age int
}
func (u User) Birthday() {
u.Age++
}
这里:
- u 是副本
- 改的是副本
- 原始 User 不变
指针接收者:操作原对象
如果写成:
func (u *User) Birthday() {
u.Age++
}
这里:
- u 是指针
- 修改的是原始结构体
但这里有个很多人没意识到的点:
用指针接收者,不只是为了“能修改”。
更重要的是:
表达这个对象是“有状态的”。
比如:
- 数据库连接
- 配置对象
- 服务实例
- 计数器
这些都不是一次性数据,它们是“同一个对象在被操作”。
这就是语义。
🔥为什么接口实现跟指针有关?
这是很多人真正卡住的地方。
看例子:
type User struct{}
func (u User) A() {}
func (u *User) B() {}
定义接口:
type I interface {
B()
}
问题来了:
var u User
var p *User
哪个能实现接口 I?
答案:
p可以u不行
为什么?
因为:
User的方法集只有 A*User的方法集有 A 和 B
所以:
指针会影响“类型的方法集合”,进而影响接口实现。
这已经不是语法问题。而是类型系统规则。
nil 是什么?为什么会 panic?
指针表达“字段可能不存在”
var p *int
fmt.Println(*p)
这里会 panic。
因为:
- p 是 nil
- nil 表示“没有指向任何内存”
- 你让程序去找一块不存在的内存
所以解引用前要判断:
if p != nil {
fmt.Println(*p)
}
设计哲学 —— 为什么 Go 不支持指针运算?
在 C 里可以:
p++
在 Go 不行。
这是设计选择。
Go 的目标是:
- 简单
- 安全
- 可预测
这也是 Go 设计者之一 Rob Pike 一直强调的哲学:
让简单的事情保持简单。
去掉指针运算,减少很多内存错误。
回到标题:什么是“语义”?
语法是:
&取地址*解引用
语义是:
- 是否共享同一块内存
- 是否修改原始对象
- 这个类型是否有状态
- 是否表达“可选值”
- 是否影响接口实现
当你开始从“共享”和“对象身份”角度理解它——
指针就不再是符号问题。
而是:
一种设计表达。
总结
记住这几句话就够了:
&是拿到变量的内存地址*是通过地址访问那块内存- 传值 = 拷贝
- 传指针 = 共享
- 指针会影响方法集和接口实现
- 用指针不是为了炫技,是为了表达“这是同一个对象”
理解到这里。
Go 指针,其实并不复杂。它只是比你想象的更“克制”。