🚀Go 指针真相:90% 的人只学会了语法,却没理解语义

0 阅读4分钟

很多人学 Go 指针,只会记住两句话:

  • & 取地址
  • * 解引用

然后代码能跑,但心里其实是懵的:

  • 取地址到底在干嘛?
  • 解引用为什么能改原值?
  • 为什么有时候必须用指针接收者?
  • 为什么接口实现又跟指针有关?

这篇我们不讲抽象。只做一件事:把“取地址”和“解引用”彻底讲透,然后自然推到语义层。

image(5).png

🔥语法层:& 和 * 到底在干什么?

下方是内存结构图,后面我们会用到。

内存:

┌──────────────────┐
│ 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

不是“指针很神奇”。而是:

你通过地址,直接改了那块内存。

8b82b9014a90f603678bcfce303a3110b051ed37.webp

🔥行为层 —— 拷贝 vs 共享

指针真正有意义,是在函数传参的时候。

默认是值拷贝

看这个:

func modify(x int) {
    x = 100
}

这里发生了什么?

  • 函数收到的是一份 拷贝
  • 修改的是拷贝
  • 原变量不受影响

因为 Go 默认是 值拷贝传递

传指针 = 共享同一块内存

再看指针版本:

func modify(x *int) {
    *x = 100
}

调用:

age := 25
modify(&age)

过程是:

  1. 把 age 的地址传进去
  2. 函数里通过 *x 找到原始内存
  3. 修改那块内存

本质区别:

传值传指针
拷贝数据共享同一块内存
改副本改原件

这就是指针的第一层意义:

共享同一块内存。

1-48.jpeg

那为什么结构体经常用指针?

值接收者:操作副本

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 一直强调的哲学:

让简单的事情保持简单。

去掉指针运算,减少很多内存错误。

回到标题:什么是“语义”?

语法是:

  • & 取地址
  • * 解引用

语义是:

  1. 是否共享同一块内存
  2. 是否修改原始对象
  3. 这个类型是否有状态
  4. 是否表达“可选值”
  5. 是否影响接口实现

当你开始从“共享”和“对象身份”角度理解它——

指针就不再是符号问题。

而是:

一种设计表达。

总结

记住这几句话就够了:

  • & 是拿到变量的内存地址
  • * 是通过地址访问那块内存
  • 传值 = 拷贝
  • 传指针 = 共享
  • 指针会影响方法集和接口实现
  • 用指针不是为了炫技,是为了表达“这是同一个对象”

理解到这里。

Go 指针,其实并不复杂。它只是比你想象的更“克制”。