Sign in

Go 指针最容易误解的,不是 * 和 &,而是函数参数的真实行为

Go 指针为什么总让人崩溃?

很多人学 Go。

前面一路顺风顺水:

然后突然。

看到这个:

*int

脑子直接短路。

再看到:

&

更懵了。

最后:

*p = 99

彻底放弃。

很多人甚至会产生一种错觉:

“我是不是不适合学系统编程?”

其实根本不是。

问题不在于指针复杂。

而在于:

大部分教程都从“语法”开始讲

但真正应该先理解的是:

内存

因为指针本质上不是语法问题。

而是:

“变量到底存在哪里”的问题。

一旦这个视角建立起来。

Go 指针其实比很多语言都简单。

甚至可以说:

非常克制。


先别急着学指针,先理解“变量”

很多教程一上来:

var p *int

然后疯狂解释:

“这是指针。”

但其实真正重要的问题是:

变量到底是什么?

例如:

var x int = 23

很多人会觉得:

“x 里面存了 23。”

这句话其实不完整。

真正发生的是:

内存里找了一块区域
把 23 放进去
然后把标签叫做 x

例如:

地址: 0xc000122020
值:   23
变量名: x

这里最关键的一点是:

x 不是 23

x 只是:

“指向那块内存的标签”。

而指针干的事情其实特别简单:

它保存的不是“值”

而是:

“值所在的位置”

也就是:

内存地址。

这一刻很多人会突然理解:

“哦,原来 pointer 本质上是 location。”

对。

它本质上就是:

地址。


真正让新手崩溃的问题:Go 函数默认永远“复制值”

这一点特别重要。

因为:

Go 所有参数传递。

默认都是:

pass by value(值拷贝)

很多人第一次看到这种代码:

func change(word string) {
    word = "world"
}

然后:

greeting := "hello"
change(greeting)

fmt.Println(greeting)

结果输出:

hello

很多新手会非常困惑:

“不是已经改了吗?”

其实没有。

因为:

函数收到的是副本

也就是说:

greeting ----复制----> word

它们根本不是同一个东西。

函数改的是:

word

而不是:

greeting

这一点是:

理解指针的真正入口。

因为指针存在的核心目的之一就是:

避免复制


指针到底是什么?

现在终于可以正式讲 pointer。

var intPtr *int

很多教程会说:

“这是 int 指针。”

但更容易理解的说法应该是:

“这是一个专门存 int 地址的变量。”

注意。

它存的不是 int。

而是:

int 所在的位置

然后:

intPtr = &x

这里:

&

意思其实特别直白:

“把 x 的地址给我”

于是:

x      -> 23
&x     -> 0xc000122020
intPtr -> 0xc000122020

这一刻:

intPtr 和 x 指向同一块内存

很多人到这里会突然顿悟:

“原来 pointer 是共享位置,不是共享值。”

完全正确。


Dereference:真正“操作地址里的东西”

拿到地址后。

下一步当然是:

读取地址里的值

这就叫:

dereference(解引用)

语法:

*intPtr

意思其实是:

“顺着地址找到真正的值”

例如:

fmt.Println(*intPtr)

输出:

23

因为:

intPtr -> 地址 -> 找到 23

更神奇的是:

*intPtr = 99

直接改掉原值。

于是:

fmt.Println(x)

输出:

99

这一刻才是真正的“魔法时刻”。

因为:

你没有直接碰 x

却改掉了 x。

本质原因是:

你们都指向:

同一块内存。


指针真正解决的问题:跨函数修改数据

现在再回来看:

为什么之前函数改不掉变量。

因为:

复制。

而 pointer 的核心作用:

就是:

让多个变量共享同一个地址

例如:

func changeValue(word *string) {
    *word = "world"
}

然后:

greeting := "hello"

changeValue(&greeting)

这里特别关键的一点:

Go 依然发生了复制。

但复制的是:

地址

而不是:

于是:

副本地址 -> 仍然指向同一块内存

这其实特别像:

复印藏宝图。

地图复制了。

但宝藏位置没变。


Go 里最容易把人搞疯的:* 有三种意思

这一点必须单独讲。

因为很多新手真正崩溃的地方就在这里。

var p *int

这里:

*

表示:

“这是指针类型”

但:

*p

这里:

*

表示:

“解引用”

而:

10 * 2

这里又是:

乘法

所以很多人第一次学 Go 时:

“为什么一个符号三种意思???”

其实关键是:

Context(上下文)

编译器会根据位置判断含义。

刚开始会乱。

但后面你的大脑会自动识别。


Maps 和 Slices 为什么“不用指针也能改”?

这是很多人后面会遇到的另一个困惑。

例如:

func update(m map[string]int) {
    m["x"] = 1
}

居然真的改掉了原 map。

为什么?

因为:

map/slice 本身已经是“带指针的包装类型”

它们内部其实自带:

pointer -> underlying data

所以:

虽然 map 被复制了。

但内部指针依然指向同一份数据。

这其实特别重要。

因为:

Go 到处都在用 pointer

只是很多时候:

语言帮你隐藏了。

Go 的哲学一直很明显:

能隐藏复杂性就隐藏

但:

需要显式控制时。

依然允许你直接操作。


什么时候该用指针?

这一点特别关键。

因为很多人学完 pointer 后。

开始:

“到处都用 *”

这是错误的。


适合用 pointer 的场景

1. 大 struct

type User struct {
    // 50 fields
}

每次复制都很贵。

pointer 更便宜。


2. 需要修改原值

例如:

func UpdateConfig(c *Config)

3. 长生命周期对象

例如:

本质上:

大家都应该操作同一份数据。


不要乱用 pointer

很多初学者会觉得:

“pointer 更高级”

其实:

小对象直接复制反而更快

例如:

int
bool
small struct

因为:

Go 会把它们放在:

stack

速度极快。

而 pointer 很可能触发:

heap allocation

然后:

GC 压力上升。

很多 Go 性能问题最后都和:

逃逸分析(Escape Analysis)

有关。


Stack vs Heap:为什么 Go 不建议“指针乱飞”

Go 有两个内存区域:

Stack

Heap

很多 pointer 会导致变量:

escape to heap

于是:

GC 压力上升。

所以:

Go 社区一直有一个非常重要的理念:

“不要为了 pointer 而 pointer”

这和 C/C++ 世界完全不同。

Go 更强调:


Go 为什么很少出现 **pointer?

最后一个特别有意思的点。

Go 几乎很少看到:

**User

也就是:

pointer to pointer。

因为:

Go 的设计哲学一直在避免:

pointer complexity explosion

如果你开始疯狂:

***something

大概率说明:

设计已经开始失控。

Go 社区特别强调:

“简单的数据流”

这也是为什么:

Go 指针虽然保留了底层能力。

但相比 C:

已经温柔太多。


真正理解指针后,你会发现它其实特别优雅

很多人第一次学 pointer。

会觉得:

“这是什么古老黑魔法。”

但真正理解后。

你会发现:

它其实只是:

“共享同一块内存”

而 Go 做的最优秀的一点是:

它既保留了:

又避免了:

这其实是 Go 特别成功的地方。

它没有彻底隐藏底层。

但也没有把复杂性全甩给开发者。

而 Pointer。

本质上只是 Go 给你的一个能力:

“别复制数据,直接告诉我它在哪。”

仅此而已。