编程语言研究书库

指针:门牌号、进屋,以及 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. 可带走的

  1. 指针 = 存地址的变量;指针的类型 = 所指向变量的类型;
  2. &变量 取门牌号,*指针 进屋取值/改值;* 在类型上是「指向」,在变量上是「进屋」;
  3. 没赋值的指针是 nil;对 nil 解引用 = 崩溃;
  4. 通过指针改值,外面的变量跟着变——传引用改值的全部机制;
  5. new(T) 运行时分配清零的内存,返回地址;新内存的初值是 T 的零值;
  6. 指针的指针存在(指向指针的指针),指针数组就是「存地址的数组」;
  7. Go 不支持指针运算,编译错;指针/整数互转、手动 free 都没有;
  8. 想让函数改外面的变量:传 &x,参数写 *T,函数里 *p = 新值;
  9. 并发下多个 goroutine 通过指针读可以,改要加锁(第 16 章);
  10. 指针的用途一句话:大数据免复制、函数改外面、运行时造无名数据。

10. 原文地图

主题原书章原文位置
指针定义与解引用Pointerstext/18-fm-learning-go-programming.txt:64(搜「hexadecimal value」)
为什么要指针、并发下的告诫Pointerstext/18-fm-learning-go-programming.txt:70(搜「large chunk of data」) · text/18-fm-learning-go-programming.txt:85(搜「quite dangerous」)
声明指针与 nil 默认值Pointerstext/18-fm-learning-go-programming.txt:97(搜「name_of_pointer」) · text/18-fm-learning-go-programming.txt:112(搜「<nil>」)
& 取地址、指针自己的地址Pointerstext/18-fm-learning-go-programming.txt:145(搜「pointerv = &a」) · text/18-fm-learning-go-programming.txt:151(搜「0x1184e068」)
解引用取值Pointerstext/18-fm-learning-go-programming.txt:169(搜「dereferencing the pointer」) · text/18-fm-learning-go-programming.txt:190(搜「// 10」)
通过指针改值Pointerstext/18-fm-learning-go-programming.txt:220(搜「*pointerv = 30」)
new 函数Pointerstext/18-fm-learning-go-programming.txt:241(搜「func new(Type)」) · text/18-fm-learning-go-programming.txt:280(搜「0x1184e09c」)
指针的指针Pointerstext/18-fm-learning-go-programming.txt:286(搜「pointer to a pointer」) · text/18-fm-learning-go-programming.txt:316(搜「0x1183e0f8」)
指针数组Pointerstext/18-fm-learning-go-programming.txt:349(搜「array of pointers」) · text/18-fm-learning-go-programming.txt:370(搜「0x11810088」)
传指针给函数(传值 vs 传引用)Pointerstext/18-fm-learning-go-programming.txt:436(搜「does not reflect」) · text/18-fm-learning-go-programming.txt:484(搜「Hello Go!」)
不支持指针运算Pointerstext/18-fm-learning-go-programming.txt:497(搜「does not support arithmetic pointers」)

Footnotes

  1. 出处:「Pointers」第 64 段(text/18-fm-learning-go-programming.txt:64,搜「hexadecimal value」)。 ↩

  2. 出处:「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

  3. 出处:「Pointers」第 88–277 段,各步的代码与输出照原书。 ↩

  4. 出处:「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」)。 ↩

  5. 出处:「Pointers」第 139–163 段(text/18-fm-learning-go-programming.txt:151,搜「0x1184e068」;text/18-fm-learning-go-programming.txt:157,搜「0x1183e0f8」)。地址值是原书运行环境的示例输出。 ↩

  6. 出处:「Pointers」第 250–280 段(text/18-fm-learning-go-programming.txt:268,搜「// 0」;text/18-fm-learning-go-programming.txt:280,搜「0x1184e09c」)。 ↩

  7. 出处:「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」)。 ↩

  8. 出处:「Pointers」第 346–373 段(text/18-fm-learning-go-programming.txt:370,搜「0x11810088」)。 ↩

  9. 出处:「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

  10. 出处:「Pointers」第 497 段(text/18-fm-learning-go-programming.txt:497,搜「does not support arithmetic pointers」)。 ↩

On this page