理解Go 的指针非常关键,并且它不是一个孤立知识点,而是和内存模型、参数传递、方法集、接口、并发安全都有关系。
做go 工程开发时,如果不能真正理解:
值传递指针传递nil pointerreceiver(方法接收者)*T和T的区别
那么写复杂服务(尤其微服务、RPC、数据库、缓存)时很容易出现隐藏 bug。
一、Go 的变量本质
先理解一个核心:
Go 函数传参永远都是值传递(pass by value)
不存在真正意义上的引用传递。
例如:
func main() {
a := 10
change(a)
fmt.Println(a)
}
func change(x int) {
x = 20
}
结果:
10
为什么?
内存:
调用前:
main:
a
|
+----+
| 10 |
+----+
调用:
change(a)
实际上复制了一份:
main:
a
|
+----+
| 10 |
+----+
change:
x
|
+----+
| 10 |
+----+
修改:
x = 20
只是改:
change.x
20
不会影响:
main.a
10
所以:
Go 的“普通”变量传参,就是复制。
二、什么是指针
指针保存的是:
一个变量的内存地址
例如:
func main() {
a := 10
p := &a
fmt.Println(p)
fmt.Println(*p)
}
func change(x *int){
*x = 20
}
输出类似:
0xc0000120a0
10
&和*这两个符号在go 语言中很重要
在 Go 语言中,& 和 * 围绕“指针”工作,核心一句话总结:
&是“取址符” :用来获取变量的内存地址(生成指针)。*是“取值符” :用来获取指针指向的实际数据(解引用);同时用于声明指针类型。
Go 函数默认是值传递(拷贝一份)。如果想让函数修改外部变量,必须传指针(&x),函数内用 * 修改
三、结构体为什么经常使用指针
例如:
type User struct {
Name string
Age int
}
创建:
u := User{
Name:"Tom",
Age:18,
}
如果:
func update(user User){
user.Age=20
}
调用:
update(u)
实际上:
复制:
u
+-------------+
| Tom | 18 |
+-------------+
user
+-------------+
| Tom | 18 |
+-------------+
修改:
user.Age=20
只改副本。
如果:
func update(user *User){
user.Age=20
}
调用:
update(&u)
传递:
user
0xc00001
|
v
u
+-------------+
| Tom | 18 |
+-------------+
修改:
u.Age=20
原对象变化。
四、什么时候使用 func(u User)
什么时候:
func (u User)
称:
值接收者(value receiver)
适合:
1. 对象很小
例如:
type Point struct{
X int
Y int
}
func(p Point) Distance(){
}
复制两个 int:
成本很低。
2. 不修改对象,只需要处理读取
例如:
func (u User) GetName() string {
return u.Name
}
只是读取。
3. 类型像值一样使用
例如:
type Money int
func(m Money) Add(x Money) Money{
}
类似:
int
float
string
五、什么时候使用 func(u *User)
绝大多数业务结构体:
推荐:
func (u *User)
原因:
- 避免复制大对象
- 需要修改对象
- 保持一致性
比如:
func(u *User) SetName()
func(u *User) Save()
func(u *User) Update()
通常一个业务对象全部使用 pointer receiver。
六、方法接收者(receiver)重点
Go:
type User struct {
}
func (u User) A(){
}
func (u *User) B(){
}
两个方法:
A:
值接收者
B:
指针接收者
区别非常大。 这不仅是语法习惯问题,更可能决定了你的程序能不能跑通、会不会报错。
核心区别总结为下面这张表:
| 对比维度 | 值接收者 A() | 指针接收者 B() |
|---|---|---|
| 能否修改原对象 | ❌ 不能。操作的是副本,原 User 不变。 | ✅ 能。操作的是原件,修改会保留。 |
| 调用时是否拷贝 | ✅ 拷贝整个结构体(大结构体性能开销大)。 | ❌ 只拷贝一个地址(8字节,极快)。 |
nil 能否调用 | ⚠️ 不能。nil 指针调用会直接崩溃(panic)。 | ✅ 能(方法内需自行判断 if u == nil)。 |
| 接口实现规则 | 实现了接口的方法集为 User 类型。 | 实现了接口的方法集为 *User 类型(这是最大的坑)。 |
最大的坑:接口实现(方法集规则)
这是Go面试最常见的问题。如果某个接口要求实现 B(),必须用 *User 能赋值给该接口,User 不行!
例如:
type Animal interface{
Speak()
}
type Dog struct{}
func(d *Dog) Speak(){
}
现在:
var a Animal
dog := Dog{}
a = dog
错误。
为什么?因为:
Dog 的方法集:
Dog:
没有 Speak()
规则:
值接收者方法:
func(T)
方法属于:
T
*T
两个都有。
指针接收者:
func(*T)
只属于:
*T
社区惯例 :
除非有极特殊原因,建议统一使用指针接收者 *User。这样既保持一致性,又能修改变量,还不用操心接口实现的坑,性能也好。只有确定方法纯只读且结构体极小时,才考虑用值接收者。
七、nil pointer(空指针)
这是生产事故高发点。
例如:
var u *User
fmt.Println(u.Name)
直接:
panic:
invalid memory address
因为:
u
nil
没有地址
正确:
if u != nil {
fmt.Println(u.Name)
}
八、为什么 Go 喜欢返回 pointer
例如:
数据库:
func GetUser(id int64) (*User,error)
而不是:
func GetUser(id int64)(User,error)
原因:
1. 表示不存在
例如:
user,nil
或者:
nil,error
2. 避免复制
User可能:
type User struct{
Profile Profile
Friends []Friend
}