GO unsafe.Pointer & uintptr
你是否经常看源码,源码里用 unsafe.Pointer, uintptr 各种骚操作,有没有想过为啥源码会这么用?若不了解 unsafe.Pointer, uintptr 使用姿势,代码很难看懂。虽然 GO 官方不建议大家使用,但作为一个 GO 工程师怎么能不了解 unsafe.Pointer 呢。
本文讲解案例采用 GO SDK 版本是 1.20.4,如果你的 GO SDK 版本较低,SDK 函数可能会有一些差异
GO 普通指针(*T)
Go 语言中的普通指针(即非 unsafe.Pointer)受到一些限制,以确保代码的安全性和可靠性,下面是普通指针的一些限制。
限制一:不能进行数学运算操作
1、 Go 语言不允许对普通指针进行数学运算,例如加法、减法等。
2、 不能直接对指针进行递增或递减操作。
func T() {
var (
x = 5
y = &x
)
y++
y = &x + 3
m := y * 2
}
上面这段代码编译会报错:
./T.go:9:2: invalid operation: y++ (non-numeric type *int)
./T.go:10:6: invalid operation: &x + 3 (mismatched types *int and untyped int)
./T.go:11:7: invalid operation: y * 2 (mismatched types *int and untyped int)
限制二:不同类型的指针不能相互转换
1、 不同类型的指针之间不能直接相互转换。
func TConvert() {
var (
n = int(100)
u *uint
)
u = &n
fmt.Println(u)
}
上面这段代码编译会报错:
cannot use &n (value of type *int) as *uint value in assignment
限制三:不能类型的指针不能用"=="、"!="比较、相互赋值
1、不同类型的指针之间不能进行比较操作,也不能相互赋值。
func Compare() {
var (
n = 100
f = 2.8
t = 100
g uint = 200
ptrN = &n
ptrF = &f
ptrT = &t
ptrG = &g
)
fmt.Printf("%v", ptrN == ptrT) // 同类型可以判等指针变量相等
fmt.Printf("%v", ptrG == ptrT) // 不同类型不能判断指针变量是否相等
fmt.Printf("%v", ptrF == ptrT) // 不同类型不能判断指针变量是否相等
}
上面这段代码编译会报错:
./T.go:37:27: invalid operation: ptrG == ptrT (mismatched types *uint and *int)
./T.go:38:27: invalid operation: ptrF == ptrT (mismatched types *float64 and *int)
2个指针变量类型相同可以相互转换的情况下,才可以进行比较。额外知识点指针变量可以通过"=="、"!="和nil进行比较
uintptr
uintptr 的定义在 builtin 包,定义如下:
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
参考注释和定义我们知道:
1、 uintptr 是 integer 类型它足够大
2、 可以存储任何一种是数据结构对应的 Pointer 地址,通俗的解释 uintptr 本质存的是地址,uintptr 存的是10进制地址,举一栗子:
func out() {
var v int
pointer := unsafe.Pointer(&v)
address := uintptr(pointer)
fmt.Println(fmt.Sprintf("vAddress=%+v,pointerAddress=%+v,address=%v", &v, pointer, address))
}
日志输出:
=== RUN TestOut
v=0xc00010e190,pointerAddress=0xc00010e190,address=824634827152
--- PASS: TestOut (0.00s)
PASS
vAddress = pointerAddress 因为指向同一个内存块,address 也是内存地址为什么是824634827152?其实很简单,pointerAddress 和vAddress 是16进制,address 是10进制
特别注意
// A uintptr is an integer, not a reference.
// Converting a Pointer to a uintptr creates an integer value
// with no pointer semantics.
// Even if a uintptr holds the address of some object,
// the garbage collector will not update that uintptr's value
// if the object moves, nor will that uintptr keep the object
// from being reclaimed.
intptr 并没有指针的语义,即使 uintptr 保存某个对象的地址,如果对象移动,uintptr 也不会阻止对象被 GC 回收。意思就是 uintptr 所指向的对象会被 gc 回收的。
unsafe 包主要提供3个函数支持【任意类型】=> uintptr 的转换:
// Sizeof takes an expression x of any type and returns the size in bytes
func Sizeof(x ArbitraryType) uintptr
// Offsetof returns the offset within the struct of the field represented by x
func Offsetof(x ArbitraryType) uintptr
// Alignof takes an expression x of any type and returns the required alignment
func Alignof(x ArbitraryType) uintptr
1、 第一个函数 Sizeof 简单好理解,获取任何类型大小返回的是字节
func sizeof() {
var v int
fmt.Println(unsafe.Sizeof(v))
}
日志输出:
=== RUN TestSizeOf
8
--- PASS: TestSizeOf (0.00s)
PASS
输出8个字节,具体场景和用法后续会拓展
2、 第二个函数Offsetof代表偏移量,主要用与struct field 偏移量
func offsetof() {
person := struct {
Name string
Age int
Address string
Phone uint
}{
Name: "李点点滴滴",
Age: 10,
Address: "ddddddd",
Phone: 12344,
}
fmt.Println(fmt.Sprintf("offsetName=%+v,offsetAge=%+v,offsetAddress=%+v,offsetPhone=%+v", unsafe.Offsetof(person.Name), unsafe.Offsetof(person.Age), unsafe.Offsetof(person.Address), unsafe.Offsetof(person.Phone)))
}
日志输出:
=== RUN TestOffsetOf
offsetName=0,offsetAge=16,offsetAddress=24,offsetPhone=40
--- PASS: TestOffsetOf (0.00s)
PASS
有内存对齐相关知识大家可以自己研究
3、 第三个函数Alignof接受任何类型的表达式x并返回所需的对齐方式,这个用的比较少了解下就行
func alignof() {
fmt.Println(unsafe.Alignof(int(0))) // 打印int类型的对齐要求
fmt.Println(unsafe.Alignof(float64(0.0))) // 打印float64类型的对齐要求
fmt.Println(unsafe.Alignof(struct{}{})) // 打印空结构体类型的对齐要求
fmt.Println(unsafe.Alignof("李四")) // 打印string的内存对其要求
}
日志输出:
=== RUN TestAlignOf
8
8
1
8
--- PASS: TestAlignOf (0.00s)
PASS
这三个函数开发者可以将任意类型变量传入获取对应的 uintptr,用来后续计算内存地址(比如基于一个结构体字段地址,获取下一个字段地址等)
unsafe.Pointer
我们看下unsafe 包下的 Pointer的定义和官方描述
// ArbitraryType is here for the purposes of documentation only and is not actually
// part of the unsafe package. It represents the type of an arbitrary Go expression.
type ArbitraryType int
// Pointer represents a pointer to an arbitrary type. There are four special operations
// available for type Pointer that are not available for other types:
// - A pointer value of any type can be converted to a Pointer.
// - A Pointer can be converted to a pointer value of any type.
// - A uintptr can be converted to a Pointer.
// - A Pointer can be converted to a uintptr.
//
// Pointer therefore allows a program to defeat the type system and read and write
// arbitrary memory. It should be used with extreme care.type
type Pointer *ArbitraryType
按照我个人的理解文档定义“ArbitraryType”是任意的类型,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 void*。
官方提供了四种 Pointer 支持的场景:
1、 任何类型的指针值都可以被转换为 Pointer
2、 Pointer 可以被转换为任何类型的指针值
3、 uintptr 可以被转换为 Pointer
4、 Pointer 可以被转换为 uintptr
unsafe.Pointer 常见几种使用技巧
将T1转换为指向T2的指针
官方用了math.Float64bits案例
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
1、 unsafe.Pointer(&f) 将 float64 类型的参数 f 的地址转换为一个 unsafe.Pointer 类型的指针
2、 *(*uint64)(unsafe.Pointer(&f)) 将 unsafe.Pointer 类型的指针转换为 *uint64 类型的指针,然后再对其进行解引用,从而将 float64 类型的值转换为 uint64 类型的整数
本质上是将unsafe.Pointer作为一种媒介,它可以由任何类型转换得到,也可以将其转换为任意类型
但这里有几点限制
1、 T2 的大小不能超过 T1
2、 T1 和 T2 必须具有相等的内存布局(即相同的字段和对齐方式)
如果满足这些条件,我们可以进行指针类型的转换。
下面这段代码int8和float32是无法转换的
func float32ToInt8(in float32) int8 {
return *(*int8)(unsafe.Pointer(&in))
}
func int8ToFloat32(in int8) float32 {
return *(*float32)(unsafe.Pointer(&in))
}
日志输出:
=== RUN TestT12T2
0
-131072
--- PASS: TestT12T2 (0.00s)
PASS
float32 是一个单精度浮点数,占用四个字节(32 位),遵循 IEEE 754 标准。它的内存布局包含符号位、指数位和尾数位。
int8 是一个有符号的 8 位整数,占用一个字节(8 位),通常以二进制补码的形式表示。
尝试将这两种类型直接强制转换会导致浮点数的部分信息丢失,或者导致整数表示的范围溢出。
将 Pointer 转换为 uintptr(不转换回 Pointer)
将指针转换为 uintptr 会得到被指向值的内存地址,以整数的形式表示。通常情况下,这样的 uintptr 用于打印或记录内存地址。
如果对Go的数组和切片有更深的了解,肯定知道数组底层的内存地址是连续的,有没有测试过呢?举一个例子:
func main() {
var x int
size := unsafe.Sizeof(x)
fmt.Printf("int 占用 %d 个字节\n", size)
tmpList := []int{1, 2, 3, 4, 5}
for i := 0; i < len(tmpList); i++ {
fmt.Printf("16进制地址=%p,10进制地址=%d,值=%+v\n", &tmpList[i], uintptr(unsafe.Pointer(&tmpList[i])), tmpList[i])
}
}
上面这段代码打印的数据如下:
=== RUN TestSizeof2
int 占用 8 个字节
16进制地址=0xc00001a1e0,10进制地址=824633827808,值=1
16进制地址=0xc00001a1e8,10进制地址=824633827816,值=2
16进制地址=0xc00001a1f0,10进制地址=824633827824,值=3
16进制地址=0xc00001a1f8,10进制地址=824633827832,值=4
16进制地址=0xc00001a200,10进制地址=824633827840,值=5
--- PASS: TestSizeof2 (0.00s)
看出来了吗?uintptr 是10进制的地址,首地址+8代表下一个下标的内存地址,这里留一个思考题,数据的下标为啥是从0开始?能推导数组下标的寻址公式吗?
将 Pointer 转换为 uintptr 并进行算术运算后再转换回 Pointer
Offsetof 获取成员偏移量
如果 p 指向一个已分配的对象,可以通过将其转换为 uintptr,加上偏移量,并将其转换回指针来在对象内进行偏移。
这种模式最常见的用法是访问结构体的字段或数组的元素。举一个例子:
func T() {
employee := struct {
Name string
Age int
}{
Name: "李四",
Age: 18,
}
// 分别打印age和name的偏移量
fmt.Printf("nameOffset=%v,ageOffset=%v\n", unsafe.Offsetof(employee.Name), unsafe.Offsetof(employee.Age))
p := unsafe.Pointer(uintptr(unsafe.Pointer(&employee)) + unsafe.Offsetof(employee.Age)) // 转为 uintptr 并且通过算术运算计算 age 的内存地址
*((*int)(p)) = 300 // Pointer 转换为 (*int)、取值、重新赋值,此时 employee.Age 值为300
fmt.Printf("age=%d\n", employee.Age)
}
上段代码打印的值:
=== RUN TestT
nameOffset=0,ageOffset=16
age=300
--- PASS: TestT (0.00s)
PASS
Sizeof 获取任意类型字节数
操作数组和struct有一些区别,再举一个数组的例子
func array() {
var (
tmpList = [6]int{2, 3, 1, 67, 8}
)
for i := 0; i < len(tmpList); i++ {
/*
1、tmpList[i] 转换为Pointer获取基地址,通过基地址+i位置角标对应值的字节数计算下一个元素的地址
2、做完算术运算后 uintptr 转 Pointer
*/
p := unsafe.Pointer(uintptr(unsafe.Pointer(&tmpList[i])) + unsafe.Sizeof(tmpList[i]))
//pp := unsafe.Add(p, unsafe.Sizeof(tmpList[i])) // 或者用这个姿势也是可以的,更简单
*(*int)(p) = i // 赋值
}
// 打印 tmpList
for i := 0; i < len(tmpList); i++ {
fmt.Printf("i=%d,v=%d\n", i, tmpList[i])
}
}
上段代码打印的值:
=== RUN TestT
i=0,v=2
i=1,v=0
i=2,v=1
i=3,v=2
i=4,v=3
i=5,v=4
--- PASS: TestT (0.00s)
PASS
数组为啥要用Sizeof?这跟数组寻址有关系,我先抛一个公式大家自行研究
a[i]_address=基地址(base_address)+i(数组下标)*字节数(int是8个字节...类推就好了)
在调用 syscall.Syscall 时将指针转换为 uintptr
基本用不着不过多解释
syscall.Syscall(syscall.SYS_PRCTL, PR_GET_KEEPCAPS, 0, 0); e != 0
将 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 的结果从 uintptr 转换为指针
func TestT(t *testing.T) {
var (
n int
)
p := unsafe.Pointer(reflect.ValueOf(&n).Pointer())
*((*int)(p)) = 3
fmt.Printf("n=%v\n", n)
}
/* === RUN TestT
n=3
--- PASS: TestT (0.00s)
PASS*/
为什么Pointer不返回Pointer而是返回的是 uintptr ?
为了防止调用者在没有导入 unsafe 包并且可能会在不了解风险的情况下,将其转换为任意类型,从而导致不安全的操作。
这种转换过程需要注意下面这点:
当你使用 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 时,返回的结果是 uintptr。为了安全地处理这个结果,你应该在调用之后立即将其转换为 unsafe.Pointer。这个转换应该在同一个表达式中完成,以避免意外的行为。下面这段代码是官方给的错误例子
// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := reflect.ValueOf(new(int)).Pointer()
p := (*int)(unsafe.Pointer(u))
将 reflect.SliceHeader 或 reflect.StringHeader 的 Data 字段与指针进行相互转换
主要是为了实现字符串和byte切片相互零拷贝转换。这个可以不用了解,官方建议在新的代码中使用 unsafe.String or unsafe.StringData、unsafe.Slice or unsafe.SliceData 。reflect.SliceHeader 和 reflect.StringHeader 应该会在后面的发布中标记为废弃。
Go1.20 引入的几个新方法
SliceData(slice []ArbitraryType) *ArbitraryType
返回指向参数切片底层数组的指针
1、 如果 slice 的容量大于 0,SliceData 返回 &slice[:1][0]。
2、 如果 slice 为 nil,SliceData 返回 nil。
3、 否则,SliceData 返回一个非空指针,指向未指定的内存地址1。
func slice2String() {
var (
b = []byte{72, 101, 108, 108, 111} // 字符串 "Hello" 对应的 ASCII 码
)
ptr := unsafe.SliceData(b)
fmt.Printf("address=%p,v=%v\n", ptr, *ptr) // 若 slice cap>0返回第一个元素指针
fmt.Printf("address=%p\n", &b[0]) // 打印第一个元素的地址
fmt.Println(unsafe.String(unsafe.SliceData(b), len(b))) // 转为字符串
}
数据的打印如下:
=== RUN TestTT
address=0xc000024288,v=72
address=0xc000024288
Hello
--- PASS: TestTT (0.00s)
PASS
String(ptr *byte, len IntegerType) string
String 函数的作用是获取一个字符串,其底层字节从指定的内存地址 ptr 开始,长度为 len。
func bytes2string() {
var (
b = []byte{72, 101, 108, 108, 111} // 字符串 "Hello" 对应的 ASCII 码
)
fmt.Println(unsafe.String(&b[0], len(b)-1))
}
数据的打印如下:
=== RUN TestTT
Hell
--- PASS: TestTT (0.00s)
PASS
StringData(str string) *byte
StringData 函数返回一个指向字符串 str 底层字节的指针。
func string2byte() {
fmt.Println(unsafe.StringData("Hello"))
fmt.Println(unsafe.StringData("Hello"))
fmt.Println(unsafe.StringData("Hello1"))
fmt.Println(*unsafe.StringData("Hello")) // 值返回第一个字符的 ASCII 码
}
数据的打印如下:
=== RUN TestTT
0x1128c69
0x1128c69
0x1128ea1
72
--- PASS: TestTT (0.00s)
PASS
为啥字符串“Hello”打印的的地址是相同的?
在Go中,相同的字符串字面量会被编译器优化为同一个内存地址,这是因为字符串是不可变的,编译器可以安全地假设它们不会被修改,因此可以共享相同的内存空间,虽然变量是不同的,但是都指向同一个字符串字面量是相同的,所以的地址是一样的。
总结
在 Go 语言中,unsafe 包提供了一种与底层系统交互的手段,以及在必要时绕过 Go 语言的类型系统进行一些底层操作。
提供了以下操作
1、 直接操作指针和内存:unsafe 包允许程序员直接操作指针,例如读写内存、修改结构体的未导出成员等
2、 绕过类型系统的限制:Go 语言的指针相比 C 的指针有一些限制,例如不能进行数学运算、不同类型的指针不能相互转换等。
3、 性能优化:在某些场景下,使用 unsafe 包可以提高代码的性能。例如,通过直接操作内存,避免了一些不必要的类型转换和拷贝操作。(大家可以学习下零拷贝技术)
思考题
1、 如何通过上面学的知识获取 slice 长度和容量?
slice 源码位置 runtime/slice.go
通过源码看到 slice 的结构体定义如下:
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
tmpList := make([]int, 1, 2),通过 make 函数 创建一个切片底层初始化方式如下:
func makeslice(et *_type, len, cap int) unsafe.Pointer
我们可以通过 unsafe.Pointer 和 uintptr 获取计算偏移量获取长度和 len,代码如下:
func TestGetSliceLen(t *testing.T) {
// 首先打印 unsafe.Pointer 占用几个字节
var (
tmpp unsafe.Pointer
)
fmt.Printf("b=%d\n", unsafe.Sizeof(tmpp))
s := make([]int, 8, 12)
plen := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8))
fmt.Printf("slice 的长度 len=%v\n", *(*int)(plen))
pcap := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16))
fmt.Printf("slice 的容量 cap=%v\n", *(*int)(pcap))
fmt.Println("---------下面是通过 len 和 cap 直接获取长度和容量")
fmt.Printf("通过 len 和 cap 语法糖获取 len=%d,cap=%d\n", len(s), cap(s))
}
日志打印如下:
=== RUN TestGetSliceLen
b=8
slice 的长度 len=8
slice 的容量 cap=12
---------下面是通过 len 和 cap 直接获取长度和容量
通过 len 和 cap 语法糖获取 len=8,cap=12
--- PASS: TestGetSliceLen (0.00s)
PASS
大家看注释,这里不过多解释
2、 unsafe.Pointer 和任意类型、unsafe.Pointer 和 uintptr 相互转换可以拆开定义多个变量吗?为什么?(TT函数是多变量方案)
func T() {
employee := Employee{}
// 和TT函数的区别在下面这几行代码
p := unsafe.Pointer(uintptr(unsafe.Pointer(&employee)) + unsafe.Offsetof(employee.Age)) // 转为 uintptr 并且通过算术运算计算 age 的内存地址
*((*int)(p)) = 300 // Pointer 转换为 (*int)、取值、重新赋值,此时 employee.Age 值为300
fmt.Printf("age=%d\n", employee.Age)
}
func TT() {
employee := Employee{}
// 和T函数的区别在下面这几行代码
p := unsafe.Pointer(&employee)
ptr := uintptr(p) + unsafe.Offsetof(employee.Age) // 转为 uintptr 并且通过算术运算计算 Age 的内存地址
pp := unsafe.Pointer(ptr) // 将 Age 内存地址转换为 Pointer
*((*int)(pp)) = 300 // Pointer 转换为 (*int)、取值、重新赋值,此时 employee.Age 值为300
fmt.Printf("age=%d\n", employee.Age)
}
type Employee struct {
Name string
Age int
}
官方回答在转换回指针之前,不能将 uintptr 存储在变量中,主要原因是在进行指针到 uintptr 的转换时,我们无法保证得到的 uintptr 值会与原始指针一一对应,并且 uintptr 类型不会提供任何指针的语义信息,也不会阻止底层对象被垃圾回收。将 uintptr 类型存储在变量中可能导致不可预料的行为,在变量重新被使用时可能造成程序的安全隐患。 顺便附带一张官方文档截图