Go 中普通指针、unsafe.Pointer 与 uintptr 之间的关系和指针运算
date
Jul 7, 2023
slug
golang-relations-of-pointer-unsafe-pointer-uintptr
status
Published
tags
Go
summary
Go 中的指针运算
type
Post
C 语言指针运算
指针运算就是对指针类型的变量做常规数学运算,例如加减操作,实现地址的偏移。指针运算在 C 语言中是原生支持的,可以直接在指针变量上做加减,例如:
结果
C 语言指针运算犹如一把双刃剑,使用得当会起到事半功倍,有神之一手的效果,反之则会产生意想不到的 bug 而且很难排查。因为在做指针运算时是比较抽象的,具体偏移了多少之后指向到了哪里是非常不直观的,可能已经偏离了设想中的位置而没有发现,运行起来就会出现错误。
例如这段 C 代码,找出数组中最小的元素:
数组中最小的是 1,可结果却是 0:
这是由于在
findMin
函数中循环条件是 i ≤ length
,超出数组大小多循环了一次,实际上数组已经越界,而 C 语言的数组实际上就是指针,C 运行时认为这是在指针运算,所以不会报错,导致数组访问到了其他内存地址,最终得到了一个错误结果。事实上有很多病毒和外挂的原理就是利用指针来访问并修改程序运行时内存数据来达到目的。例如游戏外挂可能会搜索和修改内存中的特定值,以改变玩家的生命值、金钱或其他游戏属性。通过指针运算,外挂可以直接访问这些内存位置并对其进行修改。而病毒可能使用指针运算来插入其自己的代码到一个运行中的程序,或者篡改程序的正常控制流,以达到其恶意目的。
在 C 语言之后的很多语言多多少少都对指针做了限制,例如 PHP 中的引用就可以看做是指针的简化版,而 Java 甚至干脆移除了指针。
Go 指针运算
在 Go 中默认的普通指针也是指代的是一个内存地址,值类似
0x140000ac008
,但 Go 的普通指针不支持指针运算的,例如对指针做加法会报错:报错
但 Go 还是提供了一种直接操作指针的方式,就是 unsafe.Pointer 和 uintptr。
uintptr 是一个整型,可理解为是将内存地址转换成了一个整数,既然是一个整数,就可以对其做数值计算,实现指针地址的加减,也就是地址偏移,类似跟 C 语言中一样的效果。
而 unsafe.Pointer 是普通指针和 uintptr 之间的桥梁,通过 unsafe.Pointer 实现三者的相互转换。
先看看这三位都长什么样:
输出
举一个通过指针运算修改结构体的例子
输出
再看一个操作,通过指针转换,将一个字节切片转成浮点数组:
输出
这个过程不需要 Go 的类型检查,绕过了很多流程,相对来说性能会更高。
所以大体上通过 unsafe.Pointer 的指针运算会应用在如下几个方面:
- 性能优化: 当性能是关键因素时,
unsafe
可以用来避免一些开销。例如,通过直接操作内存,可以避免切片或数组的额外分配和复制。
- C 语言交互: 当使用 cgo 与 C 语言库交互时,
unsafe
包通常用于转换类型和指针。
- 自定义序列化/反序列化: 在自定义的序列化或反序列化逻辑中,
unsafe
可以用于直接访问结构的内存布局,可以提高性能。
- 实现非标准的数据结构: 有时,特定的问题需要非标准的数据结构。
unsafe
允许你直接操作内存,可以用来实现一些 Go 的标准库中没有的数据结构。
- 反射: 与反射结合时,
unsafe
可以用于访问结构体的私有字段。