指针:门牌号、进屋,以及 Go 删掉的那些玩法
指针:门牌号、进屋,以及 Go 删掉的那些玩法
这一章讲 Go 的指针——它是 C 系语言里最让人害怕的概念,Go 把它保留了下来, 但削掉了最危险的几个玩法。 读完你会:看懂
&和*这两个符号的三种角色, 走通一个指针从生到死的主线例子,并知道 Go 和 C/C++ 在指针上的三条分界线。
1. 指针是什么:一个存门牌号的变量
程序里每个变量都住在内存的某个位置,位置有个地址(一个十六进制编号)。
指针就是存这种地址的变量;指针的类型,与它指向的变量的类型一致——
int 的指针存 int 的地址,*int 读作「指向 int 的指针」1。
书里给的「为什么要指针」四条2,用大白话收拢成一句: 数据大了,传来传去太贵;只传门牌号,谁都见得着同一份数据。
另外书里有一句重要的风险声明,原样接住: 并发是 Go 的看家本领,但多个 goroutine 通过指针读同一份数据还安全, 通过地址改数据就非常危险——「用不用、怎么用指针,全在程序员手里」2。 (怎么安全地改,是第 16 章 mutex 的活。)
2. 主线例子:a=10 的指针一生
照原书的例子,一个变量、一个指针,走完全部动作3:
出生——声明。 指针也是变量,声明方式和普通变量一样,只是类型前面多一个 *:
var pointerv *int
fmt.Println(pointerv) // → <nil>没赋值的指针,值是 nil(空)——它不指向任何地址4。
落户——初始化。 & 是「取地址」运算符:
a := 10
pointerv = &a // pointerv 现在存 a 的门牌号
fmt.Println(pointerv) // → 0x1184e068 a 的地址(照原书示例输出)
fmt.Println(&pointerv) // → 0x1183e0f8 pointerv 自己也有门牌号图说:每个变量、包括指针变量自己,都住在内存里,都有地址。
pointerv 的值是 a 的地址;&pointerv 是 pointerv 自己的地址5。
进屋——解引用(dereferencing)。 * 的另一个角色:按地址取值。
fmt.Println(*pointerv) // → 10 按门牌号进屋,取出 a 的值装修——通过指针改值。
*pointerv = 30
fmt.Println(*pointerv) // → 30
fmt.Println(a) // → 30 a 本人也变了!图说:*pointerv = 30 不是改指针,是顺着指针进屋改住户。
这就是为什么传指针能让函数改外面的变量(拆解 05 的传引用,底层就是这个动作)。
判断(我们的,不是书里的): 两个符号一身三职(
*声明指针、解引用;&取地址),是 C 系语言共有的「入门税」。记法只有一个:*出现在类型上 是「指向」,出现在变量上是「进屋」。 分清了这两个场景,所有指针语法都能读。 如果错,会错在:*还有第三个场景——乘法运算符。同一符号三种意思, 判错的成本确实高;但 Go 不允许指针运算(下节),把这个混乱砍掉了一半。
3. new:运行时分配一块内存,把门牌号给你
new(T) 在运行时分配一块 T 大小的内存,清零,返回它的地址。
pointerv = new(int)
fmt.Println(*pointerv) // → 0 新内存的零值
*pointerv = 10
fmt.Println(*pointerv) // → 10书里演示了地址的变化:先指向 a(0x1184e068),new(int) 之后指向一块新地方
(0x1184e09c)——旧指向被抛弃,垃圾回收会来收走 a 那边不再被引用的旧关系6。
new 的存在意义一句话:有些数据出生时没有名字变量,只有地址,
比如后面要讲的结构体,和并发里跨 goroutine 共享的数据。
4. 指针的指针、指针的数组
指针自己也是变量,也有地址——所以可以有「指向指针的指针」(**int 类型):
pointerv := &a // 指向 a
pointerv1 := &pointerv // 指向 pointerv
pointerv2 := &pointerv1 // 指向 pointerv1书里把三层地址逐个打印了出来(0x1184e068 → 0x1183e0f8 → 0x1183e108), 形状像一串门牌号,每格存下一格的位置7。日常代码极少手写这种, 但理解它有助于看懂后面反射、JSON 解码等库的底层报错。
指针数组:数组的每个格子存一个同类型地址。
var pointerv [4]*int
pointerv[0] = &a
pointerv[3] = &b
fmt.Println(pointerv) // → [0x11810088 <nil> <nil> 0x1181008c]没填的格子保持 nil8。
5. 传指针给函数:传引用的完整版
拆解 05 里传值和传引用各跑了一遍;本章用字符串版本把「为什么要传指针」讲透了9:
传值:changeVal(a)
函数里 y = "Hello Go!" ← 改的是复制品
出来后 a 还是 "Welcome world" ← 白改
传指针:changeVal(pointerv) // 实参是 &a
函数签名:changeVal(y *string)
函数里 *y = "Hello Go!" ← 进屋改住户
出来后 a 变成 "Hello Go!" ← 改到位了图说:两版输出的最后一句不同——传值版是 Welcome world,传指针版是 Hello Go!9。
想让函数改调用方的数据,就传 &变量,函数参数写成 *类型,函数里用 *参数 改。
6. 和 C/C++ 的三条分界线
书里只点了一条,我们补全成三条(后两条补充,不在书里,来自通用知识, 但和书里的描述不冲突):
| C/C++ 有 | Go 的态度 |
|---|---|
| 指针运算(p+1 跳到下一个元素) | 不支持,编译错 invalid operation。书里明确给了这条10 |
| 指针和整数互转、随意 reinterpret | 不支持;要绕类型系统只能用 unsafe 包(名字就是警告) |
| 手动 free() 释放 | 没有;垃圾回收负责回收不再被引用的内存 |
再加一条运行时纪律:对 nil 指针做解引用,程序 panic(崩溃)——
声明了指针没赋值就 *pointerv,等于拿着空门牌号去敲不存在的门。
这四条合起来,就是 Go 的设计立场:指针的能力留下(直接访问内存的效率), 指针的事故源头砍掉(运算、强转、手滑释放)。
7. 作者的判断与证据
| 书里的说法 | 性质 |
|---|---|
| 「指针更快、省内存、便于管理变量」 | 机制事实;「更快」在 Go 里要打折——垃圾回收和逃逸分析经常让编译器替你决定「传值还是传指针」,手写指针不等于更快 |
| 「并发下通过地址改数据很危险,明智使用是必要的」 | 事实+告诫,第 16 章的 mutex 就是对这个危险的制度化回答 |
8. 边界与局限
- 没有讲指针和切片、map 的关系:切片内部就是「指针+长度+容量」, 所以切片天然是「引用」——第 7 章的三件套在这里接上了;
- 没有讲 nil 指针 panic 的防护写法(判空
if p != nil)——见到 nil 先判空是基本功; - 没有讲 unsafe 包(书里上一章用过
unsafe.Sizeof,未展开)——知道有它、且非必要不用,即可。
9. 可带走的
- 指针 = 存地址的变量;指针的类型 = 所指向变量的类型;
&变量取门牌号,*指针进屋取值/改值;*在类型上是「指向」,在变量上是「进屋」;- 没赋值的指针是 nil;对 nil 解引用 = 崩溃;
- 通过指针改值,外面的变量跟着变——传引用改值的全部机制;
new(T)运行时分配清零的内存,返回地址;新内存的初值是 T 的零值;- 指针的指针存在(指向指针的指针),指针数组就是「存地址的数组」;
- Go 不支持指针运算,编译错;指针/整数互转、手动 free 都没有;
- 想让函数改外面的变量:传
&x,参数写*T,函数里*p = 新值; - 并发下多个 goroutine 通过指针读可以,改要加锁(第 16 章);
- 指针的用途一句话:大数据免复制、函数改外面、运行时造无名数据。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 指针定义与解引用 | Pointers | text/18-fm-learning-go-programming.txt:64(搜「hexadecimal value」) |
| 为什么要指针、并发下的告诫 | Pointers | text/18-fm-learning-go-programming.txt:70(搜「large chunk of data」) · text/18-fm-learning-go-programming.txt:85(搜「quite dangerous」) |
| 声明指针与 nil 默认值 | Pointers | text/18-fm-learning-go-programming.txt:97(搜「name_of_pointer」) · text/18-fm-learning-go-programming.txt:112(搜「<nil>」) |
| & 取地址、指针自己的地址 | Pointers | text/18-fm-learning-go-programming.txt:145(搜「pointerv = &a」) · text/18-fm-learning-go-programming.txt:151(搜「0x1184e068」) |
| 解引用取值 | Pointers | text/18-fm-learning-go-programming.txt:169(搜「dereferencing the pointer」) · text/18-fm-learning-go-programming.txt:190(搜「// 10」) |
| 通过指针改值 | Pointers | text/18-fm-learning-go-programming.txt:220(搜「*pointerv = 30」) |
| new 函数 | Pointers | text/18-fm-learning-go-programming.txt:241(搜「func new(Type)」) · text/18-fm-learning-go-programming.txt:280(搜「0x1184e09c」) |
| 指针的指针 | Pointers | text/18-fm-learning-go-programming.txt:286(搜「pointer to a pointer」) · text/18-fm-learning-go-programming.txt:316(搜「0x1183e0f8」) |
| 指针数组 | Pointers | text/18-fm-learning-go-programming.txt:349(搜「array of pointers」) · text/18-fm-learning-go-programming.txt:370(搜「0x11810088」) |
| 传指针给函数(传值 vs 传引用) | Pointers | text/18-fm-learning-go-programming.txt:436(搜「does not reflect」) · text/18-fm-learning-go-programming.txt:484(搜「Hello Go!」) |
| 不支持指针运算 | Pointers | text/18-fm-learning-go-programming.txt:497(搜「does not support arithmetic pointers」) |
Footnotes
-
出处:「Pointers」第 64 段(
text/18-fm-learning-go-programming.txt:64,搜「hexadecimal value」)。 ↩ -
出处:「Pointers」第 70–85 段(
text/18-fm-learning-go-programming.txt:70,搜「large chunk of data」;text/18-fm-learning-go-programming.txt:85,搜「quite dangerous」)。 ↩ ↩2 -
出处:「Pointers」第 88–277 段,各步的代码与输出照原书。 ↩
-
出处:「Pointers」第 106–118 段(
text/18-fm-learning-go-programming.txt:112,搜「<nil>」;text/18-fm-learning-go-programming.txt:118,搜「zero value that is nil」)。 ↩ -
出处:「Pointers」第 139–163 段(
text/18-fm-learning-go-programming.txt:151,搜「0x1184e068」;text/18-fm-learning-go-programming.txt:157,搜「0x1183e0f8」)。地址值是原书运行环境的示例输出。 ↩ -
出处:「Pointers」第 250–280 段(
text/18-fm-learning-go-programming.txt:268,搜「// 0」;text/18-fm-learning-go-programming.txt:280,搜「0x1184e09c」)。 ↩ -
出处:「Pointers」第 283–343 段(
text/18-fm-learning-go-programming.txt:316,搜「value of pointerv1」;text/18-fm-learning-go-programming.txt:343,搜「hold an address」)。 ↩ -
出处:「Pointers」第 346–373 段(
text/18-fm-learning-go-programming.txt:370,搜「0x11810088」)。 ↩ -
出处:「Pointers」第 376–487 段(
text/18-fm-learning-go-programming.txt:433,搜「Welcome world」;text/18-fm-learning-go-programming.txt:484,搜「After modification, value of a is: Hello Go!」)。 ↩ ↩2 -
出处:「Pointers」第 497 段(
text/18-fm-learning-go-programming.txt:497,搜「does not support arithmetic pointers」)。 ↩