go的指针

1 阅读4分钟

理解Go 的指针非常关键,并且它不是一个孤立知识点,而是和内存模型、参数传递、方法集、接口、并发安全都有关系。

做go 工程开发时,如果不能真正理解:

  • 值传递
  • 指针传递
  • nil pointer
  • receiver(方法接收者)
  • *TT 的区别

那么写复杂服务(尤其微服务、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)

原因:

  1. 避免复制大对象
  2. 需要修改对象
  3. 保持一致性

比如:

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

}