Go语言 unsafe.Pointer 包浅析

lifelmy_ · · 2140 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

>你必须非常努力,才能看起来毫不费力! > >微信搜索公众号[ 漫漫Coding路 ],一起From Zero To Hero ! ## 前言 在写 `Go` 的过程中,我们不免会使用指针,但是大多数情况下使用的是`类型安全`的指针,类型安全的指针有助于我们写出安全的代码,但是却有诸多限制,比如不能对地址进行算数运算、不支持任意两个类型相互转换等。 `Go` 实际上是支持`非类型安全`的指针的,通过非类型安全指针,我们可以绕过诸多限制,在某些情况下甚至可以写出更高效的代码,但同时也可能会引入一些潜在的不容易发现的问题。其次,非类型安全指针没有受到 [Go1兼容性保证](https://golang.google.cn/doc/go1compat) 的保护,在后续的Go版本中,使用非类型安全指针的代码可能会无法编译通过。 即使会有上述的风险,但目前源码的很多地方都使用了非类型安全指针,同时官方给出了正确的使用方式,本篇文章我们就一起来学习下吧! > 说明:本文中的示例,均是基于Go1.17 64位机器 ## 类型安全指针 ### 如何获得一个指针 我们有两种方式来获取类型安全的指针: 1. 通过内置函数 `new` 获取某个类型值的指针 2. 通过取地址符 `&` 获取某个变量的指针 ```go func main() { // 通过 new 为int类型的值开辟一块内存,并返回指向内存起始地址的指针 a := new(int) fmt.Printf("%p\n", a) // 0xc00034a4b8 // 通过取地址符 & ,获取一个变量的指针 b := int32(1) c := &b fmt.Printf("%p\n", c) //0xc00034a4c0 } ``` ### 为什么需要使用指针 在 `Go` 中,所有的参数传递都是值传递,没有引用传递。 1. 如果参数占用内存过大,每次函数传递都需要变量拷贝,比较耗费内存; 2. 如果我们想要在函数内部修改变量的状态,并在调用完毕后看到这种修改,就需要使用指针。 比如我们想要调用 `add` 完成变量的加一操作,但是最终并没有达到期望的效果,原因就是值传递,即调用 `add(b)` 的时候,传入的参数是 `变量b` 的一份复制,并不会影响 `main函数` 中 `变量b` 本身。 ```go func add(a int) { a = a + 1 } func main() { b := 1 add(b) println(b) // 1 } ``` 如果想要达到修改成功的目的,就需要传递指针: ```go func add(a *int) { *a = *a + 1 } func main() { a := 1 add(&a) println(a) // 2 } ``` ### 类型安全指针的限制 1. 不能对指针的地址进行算术运算 我们定义一个变量 `a` ,然后取地址,对地址算数运算 `addr++` 会编译不通过;`*addr++` 编译通过,最后输出 `a=2`,其实 `*addr++` 被编译器解释为了`(*addr)++`,即解引用操作符 `*` 的优先级 高于 `自增符++` ```go func main() { a := 1 addr := &a // addr++ 编译不通过 *addr++ // 编译通过 fmt.Println(a) // 2 } ``` 2. 两个任意指针类型不能随意转换 只有两个类型的底层数据类型是一致的,才可以完成转换 ```go type MyInt int64 type T1 *int64 type T2 *MyInt func main() { var a *int64 var myInt *MyInt var t1 T1 t1 = a // t1 是 *int64类型,a 是 *int64 类型,可以隐式转换 var t2 T2 t2 = myInt // t2 是 *MyInt类型,myInt 是 *MyInt类型,可以隐式转换 t2 = (*MyInt)(a) // t2 的底层类型是 *int64,a 是 *int64 类型,需要显式转换 t1 = (*int64)((*MyInt)(t2)) // t2 的底层类型是 *int64,t1 是 *int64类型,需要显式转换 } ``` 但是这些类型,无论怎么转换,都转换不了 `*uint64` 类型 ## unsafe包 我们说的 `非类型安全指针` 就是指 `unsafe` 包中的 `Pointer`,它被类型定义为 `type Pointer *ArbitraryType`,`ArbitraryType` 在这里仅仅是用于表示任意类型,也就是说 `Pointer` 可以指向任意数据类型,可以和任意类型的指针相互转换。 ```go // 表示任意类型 type ArbitraryType int type Pointer *ArbitraryType ``` 在上篇文章中[Go语言内存对齐详解](http://mp.weixin.qq.com/s?__biz=MzU5NzU2NDk2MA==&mid=2247485079&idx=1&sn=6e614451bb387752ff2ae344be56a859&chksm=fe50cdd8c92744ce974118b03809a11e5105e7abbc786245846aa2b8f64f5e6554659b6adb53#rd),我们也简单了解了 `unsafe` 包中有如下三个函数: 1. `func Sizeof(x ArbitraryType) uintptr` 返回一个变量占用的内存字节数 2. `func Offsetof(x ArbitraryType) uintptr` 返回结构体某个字段的地址相对于此结构体起始地址的偏移量 3. `func Alignof(x ArbitraryType) uintptr` 返回对齐系数 这三个函数的返回值的类型均为内置类型 `uintptr`,`uintptr` 是一个整数值,来保存变量的内存地址,可以和 `Pointer` 相互转换。 `Pointer` 表示指向任意类型的指针,对于该类型有四种合法的操作: - 任意类型的指针可以转为 `Pointer` - `Pointer` 可以转为任意类型的指针 - `uintptr` 可以转为 `Pointer` - `Pointer` 可以转为 `uintptr` ```go func main() { a := int(1) b := (*int64)(unsafe.Pointer(&a)) // 将 *int 先转为 Pointer,再转为 *int64 c := uintptr(unsafe.Pointer(&a)) // 将 *int 先转为 Pointer,再转为 uintptr fmt.Printf("%p\n", b) // 打印地址 0xc0003cdbb0 fmt.Printf("%x\n", c) // 地址 c0002124b8 type T struct { a string b int } t := T{a: "abc", b: 1} /* 1. 将 t 的地址转为 Pointer:符合第一种 2. 将 Pointer 转为 uintptr 后得到地址的整数值:符合第四种 3. 加上 t.b 的offset,得到 t.b 的地址整数值:uintptr是整数,可以直接相加 4. 将 uintptr 转为 Pointer:符合第三种 5. 将 Pointer 转为 *int :符合第二种 6. 最后解引用,得到具体的值 */ d := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&t)) + unsafe.Offsetof(t.b))) fmt.Println(d) // 1 } ``` Pointer 越过了类型检查,可以直接操作底层的内存,因此使用时需要格外小心。对于 Pointer的操作,只有如下六种是合法的,其余的使用方式均为非法,我们一起来看下。 ## 正确使用非类型安全指针 ### 使用方式一:利用 Pointer 作为中介,完成 T1 类型 到 T2 类型的转换 `T1` 和 `T2` 是任意类型,如果 T1 的内存占用大于等于 T2,并且 T1 和 T2 的内存布局一致,可以利用 Pointer 作为中介,完成 T1类型 到 T2类型的转换。(如果T1 的内存占用小于 T2,那么 T2 剩余部分没法赋值,就会有问题) `math` 包中的 `Float64bits` 函数将一个 `float64` 值转换为一个 `uint64 `值,`Float64frombits` 为此转换的逆转换,即 Float64bits(Float64frombits(x)) == x。 ```go func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) } func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) } ``` 如下所示,`slice` 和 `string` 结构的底层布局类似,且 slice 的内存占用大于 string,我们可以利用此种方式完成 slice 到 string 的正确转换,但是无法正确完成 string 到 slice 的转换。 ```go // slice 和 string 的底层结构 type slice struct { array unsafe.Pointer len int cap int } type stringStruct struct { str unsafe.Pointer len int } ``` ```go func main() { // slice 转 string,可以正确转换 sli := []byte{'a', 'b', 'c'} str := *(*string)(unsafe.Pointer(&sli)) fmt.Println(str) // abc fmt.Println(len(str)) // 3 // string 转 slice,cap 字段无法赋值,无法正确转换 str = "1234" b := *(*[]byte)(unsafe.Pointer(&str)) fmt.Println(string(b)) // 1234 fmt.Println(len(b)) // 4 fmt.Println(cap(b)) // 824634066744 } ``` slice 转为 string 后,两者对应的指针指向的是同一个字节数组,因此修改底层的数组值,string 相应的也会跟着改变。 ```go func main() { // 字节数组转字符串 sli := []byte{'a', 'b', 'c'} str := *(*string)(unsafe.Pointer(&sli)) fmt.Println(str) // abc fmt.Println(len(str)) // 3 sli[0] = 'd' sli[1] = 'e' fmt.Println(str) // dec } ``` ### 使用方式二:将 Pointer 转为 uintptr (不再转回 Pointer) 将 `Pointer` 转为 `uintptr`,并且不再转回 `Pointer`,此方式用处不大,通常我们只用来打印值。 此方式相当于取变量的内存地址,由于 `uintptr` 是个变量值,而非引用,后续该变量被移动到其他位置,其对应的`uintptr`值不会更新;其次,如果后续没有使用该变量,随时可能会被垃圾回收掉。 ```go // 每次运行得到的内存地址,可能不一样 func main() { a := int(10) fmt.Printf("%p\n", &a) // 0xc0001184b8 fmt.Printf("%x\n", uintptr(unsafe.Pointer(&a))) // c0001184b8 } ``` 因此,将 uintptr 转回 Pointer 是存在风险的,只有接下来我们列举的几种转换方式合法的。 ### 使用方式三:将Pointer转为 uintptr,然后再通过算数方式将 uintptr 转回 Pointer 我们可以将一个变量的 `Pointer` 转为 `uintptr`,然后再加上一定的偏移量转回 `Pointer`,这种方式通常用来获取结构体中的成员变量地址或者数组中第i个元素的地址。 结构体:我们可以先拿到结构体变量 `e` 的地址,然后加上 `成员b` 的偏移量,就可以得到 `e.b` 的地址,再转回 `Pointer` 就能够拿到对应的值了。 ```go func main() { type Example struct { a int32 b string } e := Example{ a: 1, b: "test", } // 等价于 *(*string)(unsafe.Pointer(&e.b)) c := *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b))) fmt.Println(c, d) } ``` 数组:拿到了数组第一个元素 `a[0]` 的地址,转为 `uintptr` 后,加上 `2倍` 个元素类型占用的内存大小,就可以得到第 `3` 个元素的地址值,再转回 `Pointer`,最后转为 `int`,就得到了`第三个`元素的值。 ```go func main() { a := []int{1, 2, 3, 4} b := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a[0])) + 2*unsafe.Sizeof(a[0]))) fmt.Println(b) } ``` 同理,获取一个成员或元素的地址,然后减去相应的偏移量,也是合法操作。但是无论怎么操作,需要保证最后得到的地址,是在当前变量占用的地址范围内,不能超出,如下几种就是非法的操作: - 非法操作一:超出变量内存范围 ```go // 从初始地址,最多加 unsafe.Sizeof(s)-1 var s thing end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s)) ``` ```go // 声明了 n 个字节的长度,从初始地址最多加 n-1 b := make([]byte, n) end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n)) ``` - 非法操作二:使用变量保存 uintptr 的值 在将 `uintptr` 类型转为 `Pointer` 类型之前,不能将 `uintptr` 的的值赋值给变量 ```go // 非法操作示例 func main() { type Example struct { a int32 b string } e := Example{ a: 1, b: "test", } // 正确操作 c := *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b))) addr := uintptr(unsafe.Pointer(&e)) + unsafe.Offsetof(e.b) // 到这里,变量 e 没有任何引用了,因此可能随时被垃圾回收器回收,一旦被回收,再使用 e.b 原来的地址将是非常危险的 c := *(*string)(unsafe.Pointer(addr)) fmt.Println(c) 」 ``` - 非法操作三:Pointer 指向 nil `Pointer` 需要指向一个分配过内存的变量,不能指向 `nil` ```go // Pintere指向nil是非法的 u := unsafe.Pointer(nil) p := unsafe.Pointer(uintptr(u) + offset) ``` ### 使用方式四:将 `Pointer` 转为 `uintptr`, 传递给系统调用 `syscall.Syscall` 我们知道 `uintptr` 是一个整数,获取到了一个变量的 `uintptr` 值,并不能保证变量不被垃圾回收掉,如果变量被垃圾回收掉,使用原先的 `uintptr` 值将是非常危险的。 下面这个函数是危险的原因在于,函数本身不能保证传递进来的地址对应的内存块一定没有被回收。 如果此内存块已经被回收了或者被重新分配给了其它变量,那么此函数内部的操作将是非法和危险的。 ```go func DoSomething(addr uintptr) { // 对处于传递进来的地址处的值进行读写... } ``` 然而系统调用则有这种特权,保证了地址对应的内存块在函数执行过程中不被回收和移动。例如 `syscall` 标准库包中的 `Syscall` 函数的原型为: ``` func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) ``` 那么此函数是如何保证传递给它的地址参数值`a1`、`a2`和`a3`处的内存块在执行过程中一定没有被回收和被移动呢? 此函数无法做出这样的保证,事实上,是编译器做出了这样的保证。 这是 `syscall.Syscall` 这样函数的特权,其它自定义函数无法享受到这样的待遇。 正确的使用姿势为: ```go // 将 p 对应的 Pointer 值转为 uintptr syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n)) ``` 同时需要注意的是,我们也不能先将 `uintptr` 的值赋值给一个变量,然后再传入 `syscall.Syscall` ```go u := uintptr(unsafe.Pointer(p)) // 此时 p 可能被回收或者移动 syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n)) ``` ### 使用方式五:将 `reflect.Value.Pointer` 或者 `reflect.Value.UnsafeAddr` 的 `uintptr` 值转为 `unsafe.Pointer` `reflect`包中,`Value` 类型的 `Pointer `和 `UnsafeAddr `方法都返回一个 `uintptr` 值,而不是 `unsafe.Pointer` 值,这样做是为了避免用户在没有引入 `unsafe` 包的条件下,就可以将这两个方法的返回值转为任意类型安全的指针。(比如返回值 a 是 unsafe.Pointer 类型,不引入unsafe包,可以直接进行(*int32)(a),将其转为 int32 类型的指针 )。 因此,这种设计需要我们在调用完 `reflect.Value.Pointer` 或者 `reflect.Value.UnsafeAddr`后,立即调用 `unsafe.Pointer` 转为 `Pointer` 类型,否则在调用的空窗期,变量可能被移动或者回收。 ```go func main() { type Example struct { a int32 b string } e := Example{ a: 1, b: "test", } // 1. 正确使用方式 b := *(*string)(unsafe.Pointer(reflect.ValueOf(&e.b).Pointer())) fmt.Println(b) // test // 2. 错误使用方式 p := reflect.ValueOf(&e.b).Pointer() // 此时变量可能被移动或者回收 b = *(*string)(unsafe.Pointer(p)) fmt.Println(b) } ``` ### 使用方式六:将 `reflect.SliceHeader` 或者 `reflect.StringHeader` 的 `Data` 域对应的 `uintptr` 转为 `Pointer`,或者将其他 `Pointer` 转为 `uintptr` 赋值给 `Data` `slice` 和 `string` 底层的数据结构如下:其中 `slice` 结构的 `array` 字段和 `string` 结构的 `str` 字段底层其实都指向 `字节数组`。 `SliceHeader` 和 `StringHeader` 分别是 `slice` 和 `string` 结构的运行时表示,对于任意一个 `slice` 或者 `string`,我们可以拿到它的运行时表示,然后修改其 `Data` 值,达到修改其底层数据的目的。即我们可以将一个字符串的指针值 转换为 `*reflect.StringHeader` ,进而可以对此字符串的内部进行修改。类似,我们也可以将一个切片的指针值转换为 `*reflect.SliceHeader` ,从而对此切片的内部进行修改。 这样做的好处是,在不重新分配内存的情况下,将 `string` 或 `slice` 的底层数据改变。 ```go type slice struct { array unsafe.Pointer len int cap int } type stringStruct struct { str unsafe.Pointer len int } type SliceHeader struct { Data uintptr Len int Cap int } type StringHeader struct { Data uintptr Len int } ``` 和上面第五条同样的原因,为了避免用户没有引入 `unsafe包` 就可以直接转换, `reflect.SliceHeader` 或者 `reflect.StringHeader` 的 `Data` 域都是 `uintptr` 类型。 ```go // 修改字符串对应的Data域 func main() { str := "test" // 字节数组,修改后字符串底层数据指向这个数组 a := [3]byte{'a', 'b', 'c'} strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str)) strHeader.Data = uintptr(unsafe.Pointer(&a)) strHeader.Len = len(a) fmt.Println(str) // abc } ``` ```go func main() { sli := []byte{'h', 'e', 'l', 'l', 'o'} array := [4]byte{'1', '2', '3', '4'} // 将切片转为 reflect.SliceHeader 结构 sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&sli)) // 修改对应的字段数据,修改后 sli 底层的数据指向了 array sliceHeader.Data = uintptr(unsafe.Pointer(&array)) // 先设置长度为2 sliceHeader.Len = 2 sliceHeader.Cap = len(array) fmt.Printf("%s\n", sli) // 12 // 修改 sli 的长度 sli = sli[:cap(sli)] fmt.Printf("%s\n", sli) // 1234 } ``` 一般来说,我们应该从一个已经存在的字符串得到 `*reflect.StringHeader`,或者从一个已经存在的切片得到 `*reflect.SliceHeader`,不能直接声明 `reflect.SliceHeader` 或 `reflect.StringHeader` 变量: ```go // 错误使用方式 var hdr reflect.StringHeader hdr.Data = uintptr(unsafe.Pointer(new([5]byte))) // 在此时刻,上一行代码中刚开辟的数组内存块已经不再被任何值所引用,所以它可以被回收 hdr.Len = n s := *(*string)(unsafe.Pointer(&hdr)) // 危险 ``` 使用 `reflect.SliceHeader` 和 `reflect.StringHeader`,我们可以在不重新分配底层数据内存的情况下,完成 `slice` 和 `string` 类型互换: ```go // 字节切片转 string func ByteSlice2String(slice []byte) (s string) { sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) stringHeader.Data = sliceHeader.Data stringHeader.Len = sliceHeader.Len return } // string 转字节切片 func String2ByteSlice(s string) (slice []byte) { stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice)) sliceHeader.Data = stringHeader.Data sliceHeader.Len = stringHeader.Len sliceHeader.Cap = stringHeader.Len return } func main() { b := []byte{'h', 'e', 'l', 'l', 'o'} fmt.Println(ByteSlice2String(b)) // hello s := "hello" fmt.Println(String2ByteSlice(s)) // [104 101 108 108 111] } ``` 由于默认字符串内存是分配在不可修改区的,使用上述的 `String2ByteSlice `将 `string` 转为 `slice` 后,只能进行读取,不能修改其底层数据值: ```go func main() { s1 := "Goland" // 官方标准编译器会将 s1 的字节开辟在不可修改内存区 b1 := String2ByteSlice(s1) // 转为字节数组 fmt.Printf("%s\n", b1) // Goland // 由于字符串 s1 底层指向的字节数组在不可修改区,此时不能修改值,否则会panic // b1[5] = 'a' // 这种方式不会存放在不可修改区,转为字节数组后,可以修改值 s2 := strings.Join([]string{"Go", "land"}, "") b2 := String2ByteSlice(s2) fmt.Printf("%s\n", b2) // Goland b2[5] = 'g' // 相当于修改底层数组的值,原字符串的值也会随之改变 fmt.Println(s2) // Golang } ``` ## 总结 本篇文章从类型安全指针切入,介绍了如何获取指针、为什么需要使用指针以及类型安全指针的局限性,然后进一步介绍了 `unsafe` 包中对于非类型安全指针类型 `Pointer` 的定义以及使用方法,最后通过具体示例详细介绍了六种正确使用 `Pointer` 的场景。 ## 更多 个人博客: https://lifelmy.github.io/ 微信公众号:漫漫Coding路

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2140 次点击  ∙  1 赞  
加入收藏 微博
1 回复  |  直到 2022-04-17 11:29:52
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传