3.2 指针:从取地址到unsafe边界


3.2 指针:从取地址到 unsafe 边界

本节摘要:Go 保留指针但砍掉指针运算,用取地址符与解引用符完成"间接指向"。本节讲清值传递的本质、指针与函数参数/数组/结构体/切片/map 的组合行为、new 与取地址的等价性、二级指针的使用边界,以及 unsafe 包打开的底层通道与代价——为第 6 章的 unsafe 深潜做铺垫。

本节导读

阅读完本节,你应当能够:

  1. 解释"Go 一切传值"与指针传地址的关系;
  2. 用指针参数让函数修改调用方的变量;
  3. 说出切片、map 为什么天然等效"引用传递";
  4. 区分指针的零值 nil 与野指针概念;
  5. 说明 unsafe.Pointer 存在的意义与三条转换规则。

一、先立地基:Go 只有值传递

函数调用永远拷贝实参。指针的价值在于:拷贝一份地址,比拷贝整个结构体便宜得多,而且顺着地址能改到原件。

func swap(a, b *int) { *a, *b = *b, *a // 解引用,改的是调用方的变量 } x, y := 1, 2 swap(&x, &y) // 取地址传入

三个符号各司其职:& 取地址,* 在类型位置表"指向某类型的指针",在表达式位置表解引用。没有 C 的算术(不能 p+1)、没有 ->(自动透过指针访问字段,p.Name 直接可用),安全性大幅提升。

type User struct{ Name string; Age int } u := User{Name: "Ada", Age: 36} p := &u p.Age = 37 // 等效 (*p).Age,语法糖

二、指针与各类型的组合行为

类型 传值给函数后 想改原件要用
基本类型 int 等 拷贝,改不到原件 *int 参数
数组 整块拷贝 *数组 或改用切片
结构体 字段全拷贝 *结构体(最常用)
切片 拷贝三元组头,底层数据共享 通常不需要;要改 len 才传 *切片
map 拷贝头指针,桶共享 通常不需要
指针本身 拷贝地址值 **T(二级指针,罕见)

切片与 map 因为头里带指针,天然"改内容生效、改头不生效"。由此有个经典坑:函数内向 nil 切片 append 后返回,调用方却没接返回值——头是拷贝的,新头到不了调用方。

二级指针几乎只出现在"让函数初始化调用方的指针变量"这类罕见需求,或某些 C 互操作场景,业务代码见到它多半是设计问题。

三、new 与零值可用

new(T) 分配零值并返回指针,var p *T 加赋值、或直接 &T{} 结构体字面量取地址,效果相近。现代 Go 代码里结构体一律 &User{...},new 主要服务非结构体类型与泛型分配。指针零值是 nil,解引用 nil 会 panic——Go 没有"野指针悬垂"的常规来源(GC 保证对象活着就有指针有效性),内存安全的最大敌人只剩 nil 本身。

四、函数返回指针:逃逸分析登场

func newUser() *User { u := User{Name: "局部变量"} return &u // 合法!GC接手,不会悬垂 }

从 C 来的人会本能觉得"返回局部变量地址"危险。Go 里编译器的逃逸分析发现 u 被带出函数,就把他分配到堆上。代价是堆分配加 GC 压力——高频小对象场景,值语义反而是性能优化。第 6 章工具链会展示如何用编译器选项观察逃逸决策。

五、指针与 unsafe:一扇窄门

正常指针不能转换类型(*int 不能转 *string),但经 unsafe.Pointer 这座桥可以。它是与任何指针双向转换的特殊类型,再配合 uintptr 做数值地址。三步链条:

var i int = 10 p := &i up := unsafe.Pointer(p) // 任意指针 → unsafe.Pointer num := uintptr(up) // → 数值地址(仅瞬时使用) _ = num

unsafe 包同时提供四个"只读探针":Sizeof 量类型字节数(如 64 位平台上 int 是 8、string 头是 16,因为含指针加长度两个字)、Alignof 量对齐、Offsetof 量字段偏移;新版本还加了 Add 与 Slice 等受限工具。这些是第 6 章 unsafe 专节的主角,这里只立一个观念:

⚠️ 常见坑:uintptr 只是整数,GC 不认它。把地址长期存在 uintptr 里、期间对象可能被搬走或回收,再转回指针访问就是未定义行为。规则是"指针到数值、用完立刻回来",中间不许藏变量。

六、什么时候用指针:三条判断

  1. 方法需要修改接收者 → 指针接收者(下一节展开)。
  2. 结构体较大且高频传递 → 传指针省拷贝;但小结构体值传递反而更快(栈上零成本)。
  3. 需要表达"没有值" → 指针天然可为 nil。注意这与第 4 章错误处理的"值加错误"模式天然契合。

反过来,并发共享的可变指针是数据竞争的温床——指针给了你"改原件"的能力,也就给了你"同时改原件"的能力。第 5 章整章都在讲怎么管住这件事。

💡 关键直觉:把指针当成"遥控器"而不是"地址"——遥控器可以复制给很多人,但被遥控的电视只有一台;谁都能按,同时按就打架。

七、指针传递决策图

补几个判断问答。方法接收者与函数参数的指针选择要一致吗? 尽量一致(第 3.3 节的方法集规则),混用会在装接口时咬人。指针池化优化值得吗? 先用第 6 章的剖析证明分配真是热点再谈,否则是负优化。& 取的地址什么时候失效? 永不"悬垂"——GC 保证可达对象不动声色地续命;你能犯的错只有 nil 解引用。两个指针能比较吗? 能,== 判"是否指向同一对象";结构体指针比内容要解引用逐字段比(或用反射的深度相等,见第 6 章)。

要点串联

  • 一切传值:指针传递的是地址的拷贝,顺着它能改到原件。
  • 三个符号:& 取地址、* 类型处表指针、表达式处解引用。
  • 切片与 map:头部带指针,天然共享底层数据;改头仍需指针或返回值。
  • 无指针运算、无悬垂指针:nil 是唯一要防的敌人。
  • 逃逸分析:返回局部变量地址合法,代价是堆分配。
  • unsafe.Pointer 是窄门:类型擦除的桥,uintptr 只许瞬时使用。
  • 并发警示:共享可变指针等于埋雷,配套同步机制见第 5 章。

下一节把行为挂到类型上:方法与接口。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U