Go语言 unsafe.Pointer 浅析
Posted 漫漫Coding路
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言 unsafe.Pointer 浅析相关的知识,希望对你有一定的参考价值。
前言
在写 Go
的过程中,我们不免会使用指针,但是大多数情况下使用的是类型安全
的指针,类型安全的指针有助于我们写出安全的代码,但是却有诸多限制,比如不能对地址进行算数运算、不支持任意两个类型相互转换等。
Go
实际上是支持非类型安全
的指针的,通过非类型安全指针,我们可以绕过诸多限制,在某些情况下甚至可以写出更高效的代码,但同时也可能会引入一些潜在的不容易发现的问题。其次,非类型安全指针没有受到 Go1兼容性保证 的保护,在后续的Go版本中,使用非类型安全指针的代码可能会无法编译通过。
即使会有上述的风险,但目前源码的很多地方都使用了非类型安全指针,同时官方给出了正确的使用方式,本篇文章我们就一起来学习下吧!
类型安全指针
如何获得一个指针
我们有两种方式来获取类型安全的指针:
- 通过内置函数
new
获取某个类型值的指针 - 通过取地址符
&
获取某个变量的指针
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
中,所有的参数传递都是值传递,没有引用传递。
- 如果参数占用内存过大,每次函数传递都需要变量拷贝,比较耗费内存;
- 如果我们想要在函数内部修改变量的状态,并在调用完毕后看到这种修改,就需要使用指针。
比如我们想要调用 add
完成变量的加一操作,但是最终并没有达到期望的效果,原因就是值传递,即调用 add(b)
的时候,传入的参数是 变量b
的一份复制,并不会影响 main函数
中 变量b
本身。
func add(a int)
a = a + 1
func main()
b := 1
add(b)
println(b) // 1
如果想要达到修改成功的目的,就需要传递指针:
func add(a *int)
*a = *a + 1
func main()
a := 1
add(&a)
println(a) // 2
类型安全指针的限制
- 不能对指针的地址进行算术运算
我们定义一个变量 a
,然后取地址,对地址算数运算 addr++
会编译不通过;*addr++
编译通过,最后输出 a=2
,其实 *addr++
被编译器解释为了(*addr)++
,即解引用操作符 *
的优先级 高于 自增符++
func main()
a := 1
addr := &a
// addr++ 编译不通过
*addr++ // 编译通过
fmt.Println(a) // 2
- 两个任意指针类型不能随意转换
只有两个类型的底层数据类型是一致的,才可以完成转换
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
可以指向任意数据类型,可以和任意类型的指针相互转换。
// 表示任意类型
type ArbitraryType int
type Pointer *ArbitraryType
在上篇文章中Go语言内存对齐详解,我们也简单了解了 unsafe
包中有如下三个函数:
-
func Sizeof(x ArbitraryType) uintptr
返回一个变量占用的内存字节数
-
func Offsetof(x ArbitraryType) uintptr
返回结构体某个字段的地址相对于此结构体起始地址的偏移量
-
func Alignof(x ArbitraryType) uintptr
返回对齐系数
这三个函数的返回值的类型均为内置类型 uintptr
,uintptr
是一个整数值,来保存变量的内存地址,可以和 Pointer
相互转换。
Pointer
表示指向任意类型的指针,对于该类型有四种合法的操作:
- 任意类型的指针可以转为
Pointer
Pointer
可以转为任意类型的指针uintptr
可以转为Pointer
Pointer
可以转为uintptr
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 := Ta: "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。
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 的转换。
// slice 和 string 的底层结构
type slice struct
array unsafe.Pointer
len int
cap int
type stringStruct struct
str unsafe.Pointer
len int
func main()
// slice 转 string,可以正确转换
sli := []bytea, 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 相应的也会跟着改变。
func main()
// 字节数组转字符串
sli := []bytea, 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
值不会更新;其次,如果后续没有使用该变量,随时可能会被垃圾回收掉。
// 每次运行得到的内存地址,可能不一样
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
就能够拿到对应的值了。
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
,就得到了第三个
元素的值。
func main()
a := []int1, 2, 3, 4
b := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a[0])) + 2*unsafe.Sizeof(a[0])))
fmt.Println(b)
同理,获取一个成员或元素的地址,然后减去相应的偏移量,也是合法操作。但是无论怎么操作,需要保证最后得到的地址,是在当前变量占用的地址范围内,不能超出,如下几种就是非法的操作:
- 非法操作一:超出变量内存范围
// 从初始地址,最多加 unsafe.Sizeof(s)-1
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))
// 声明了 n 个字节的长度,从初始地址最多加 n-1
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))
- 非法操作二:使用变量保存 uintptr 的值
在将 uintptr
类型转为 Pointer
类型之前,不能将 uintptr
的的值赋值给变量
// 非法操作示例
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
// Pintere指向nil是非法的
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + offset)
使用方式四:将 Pointer
转为 uintptr
, 传递给系统调用 syscall.Syscall
我们知道 uintptr
是一个整数,获取到了一个变量的 uintptr
值,并不能保证变量不被垃圾回收掉,如果变量被垃圾回收掉,使用原先的 uintptr
值将是非常危险的。
下面这个函数是危险的原因在于,函数本身不能保证传递进来的地址对应的内存块一定没有被回收。 如果此内存块已经被回收了或者被重新分配给了其它变量,那么此函数内部的操作将是非法和危险的。
func DoSomething(addr uintptr)
// 对处于传递进来的地址处的值进行读写...
然而系统调用则有这种特权,保证了地址对应的内存块在函数执行过程中不被回收和移动。例如 syscall
标准库包中的 Syscall
函数的原型为:
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
那么此函数是如何保证传递给它的地址参数值a1
、a2
和a3
处的内存块在执行过程中一定没有被回收和被移动呢? 此函数无法做出这样的保证,事实上,是编译器做出了这样的保证。 这是 syscall.Syscall
这样函数的特权,其它自定义函数无法享受到这样的待遇。
正确的使用姿势为:
// 将 p 对应的 Pointer 值转为 uintptr
syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))
同时需要注意的是,我们也不能先将 uintptr
的值赋值给一个变量,然后再传入 syscall.Syscall
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
类型,否则在调用的空窗期,变量可能被移动或者回收。
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
的底层数据改变。
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
类型。
// 修改字符串对应的Data域
func main()
str := "test"
// 字节数组,修改后字符串底层数据指向这个数组
a := [3]bytea, b, c
strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str))
strHeader.Data = uintptr(unsafe.Pointer(&a))
strHeader.Len = len(a)
fmt.Println(str) // abc
func main()
sli := []byteh, e, l, l, o
array := [4]byte1, 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
变量:
// 错误使用方式
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
类型互换:
// 字节切片转 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 := []byteh, e, l, l, o
fmt.Println(ByteSlice2String(b)) // hello
s := "hello"
fmt.Println(String2ByteSlice(s)) // [104 101 108 108 111]
由于默认字符串内存是分配在不可修改区的,使用上述的 String2ByteSlice
将 string
转为 slice
后,只能进行读取,不能修改其底层数据值:
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语言 unsafe.Pointer 浅析的主要内容,如果未能解决你的问题,请参考以下文章