一、前言
相信看过Go源码的同学已经对unsafe.Pointer非常的眼熟,因为这个类型可以说在源码中是随处可见:map、channel、interface、slice…但凡你能想到的内容,基本都会有unsafe.Pointer的影子。
看字面意思,unsafe.Pointer是“不安全的指针”,指针就指针吧,还安不安全的是个什么鬼?
二、指针
为了更深入了解unsafe.Pointer,我们首先讲解一下指针类型。
2.1 指针的运用
为什么需要指针类型呢?参考文献 go101.org 里举了这样一个例子:
package main
import "fmt"
func double(x int) {
x += x
}
func main() {
var a = 3
double(a)
fmt.Println(a) // 3
}
我想在 double 函数里将 a 翻倍,但是例子中的函数却做不到。为什么?因为 Go 语言的函数传参都是 值传递。double 函数里的 x 只是实参 a 的一个拷贝,在函数内部对 x 的操作不能反馈到实参 a。
如果这时,有一个指针就可以解决问题了!这也是我们常用的“伎俩”。
package main
import "fmt"
func double(x *int) {
*x += *x
}
func main() {
var a = 3
double(&a)
fmt.Println(a) // 6
p := &a
double(p)
fmt.Println(a, p == nil) // 12 false
}
说明:
*x += *x,这一句把 x 指向的值(也就是 &a 指向的值,即变量 a)变为原来的 2 倍。但是对 x 本身(一个指针)的操作却不会影响外层的 a,所以 x=nil掀不起任何大风大浪。
2.2 指针的限制
然而,相比于 C 语言中指针的灵活,Go 的指针多了一些限制。但这也算是 Go 的成功之处:既可以享受指针带来的便利,又避免了指针的危险性,限制性如下:
Go 中指针不能进行算术运算。例如:&a++
Go 中不同类型的指针不能相互转换。例如:var a int = 1;f := (*float64)(&a)
Go 中不同类型的指针不能比较和相互赋值,例如:var a int = 1;var f float64;f = &a;&a == &f
以上指针错误的使用方式在 Go 中都会编译报错。
三、unsafe.Pointer 是什么
unsafe.Pointer可以指向任意类型的指针。不能进行指针运算,不能读取内存存储的值(想读取的话需要转成相对应类型的指针)。它是桥梁,让任意类型的指针实现相互转换, 也可以转换成uintptr 进行指针运算。
Pointer是在unsafe的package里。源代码中的比较多,摘取部分如下:
type Pointer *ArbitraryType
Pointer represents a pointer to an arbitrary type. There are four special operations
available for type Pointer that are not available for other types: // Pointer代表了一个任意类型的指针。Pointer类型有四种特殊的操作是其他类型不能使用的:
- A pointer value of any type can be converted to a Pointer. // 任意类型的指针可以被转换为Pointer
- A Pointer can be converted to a pointer value of any type. // Pointer可以被转换为任务类型的值的指针
- A uintptr can be converted to a Pointer. // uintptr可以被转换为Pointer
- A Pointer can be converted to a uintptr. // Pointer可以被转换为uintptr
Pointer therefore allows a program to defeat the type system and read and write
arbitrary memory. It should be used with extreme care. // 因此Pointer允许程序不按类型系统的要求来读写任意的内存,应该非常小心地使用它。
一般的指针运算有三个步骤。
1.将unsafe.Pointer转换为uintptr
2.对uintptr执行算术运算
3.将uintptr转换回unsafe.Pointer,然后转成访问指向的对象的指针类型。
3.1 什么是 uintptr
源码地址:src/builtin/builtin.go:
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
uintptr 是一个整数类型,足够大能保存任何一种指针类型。uintptr 指的是具体的内存地址,不是个指针,因此 uintptr 地址关联的对象可以被垃圾回收。而 unsafe.Pointer 有指针语义,可以保护它不会被垃圾回收。
指针类型、unsafe.Pointer、uintptr 三者关系如下:
指针类型 *T <-> unsafe.Pointer <-> uintptr
uintptr 是可用于存储指针的整型,而整型是可以进行数学运算的。因此,将 unsafe.Pointer 转化为 uintptr 类型后,就可以让本不具备运算能力的指针具备了指针运算能力。
3.2 总结
unsafe.Pointer可以和任意的指针类型进行转换,意味着可以借助 unsafe.Pointer 完成不同指针类型之间的转换。
unsafe.Pointer 可以转换为 uintptr,而 uintptr 拥有计算能力,因此指针可以借助 unsafe.Pointer 和 uintptr 完成算术运算,进而直接操作内存。
四、unsafe.Pointer 实战
unsafe.Pointer 可以在不同的指针类型之间做转化,从而可以表示任意可寻址的指针类型,利用 unsafe.Pointer 为中介,即可完成指针类型的转换。先举一个简单例子,看一下不同指针类型之间的转换过程:
package main
import (
"fmt"
"unsafe"
)
func main() {
i := 100
intI := &i
var floatI *float64
floatI = (*float64)(unsafe.Pointer(intI))
*floatI = *floatI * 3
fmt.Printf("%T\n", i)
fmt.Println(i)
fmt.Printf("%T\n", intI)
fmt.Printf("%T\n", floatI)
}
// 输出
int
300
*int
*float64
该例子中定义了两个指针变量分别是 *int 类型的 intI 和 *float64 类型的 floatI,然后先对 intI 做了类型 unsafe.Pointer 的转换,随后进行 *float64 类型的转换;然后对 *float64 进行乘法操作,最终影响到了 i 变量,也从侧面证明了 *float64 的指针变量是指向 i 变量的内存地址的。
然后我们看一个类型转换经典的例子:实现string和 slice之间的转换,要求是零拷贝。如果按照以往的方式,循环遍历,然后挨个拷贝赋值是无法完成目标的,这个时候只能考虑共享底层 []byte 数组才可以实现零拷贝转换。string 和 []byte 在运行时的类型表示为 reflect.StringHeader 和 reflect.SliceHeader
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
上面是反射包下的结构体,路径:src/reflect/value.go。只需要共享底层 []byte 数组就可以实现 zero-copy。
使用 unsafe.Pointer 将 string 或 []byte 转换为 *reflect.StringHeader 或 *reflect.SliceHeader,然后通过构造方式,完成底层 []byte 数组的共享,最后通过指针类型转换方式再次转换回来,代码如下:
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func bytes2string(b []byte) string{
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: sliceHeader.Data,
Len: sliceHeader.Len,
}
return *(*string)(unsafe.Pointer(&sh))
}
案例一:通过指针运算,修改数组内部的值。
package main
import (
"fmt"
"unsafe"
)
func main() {
arr := [3]int{1, 2, 3}
ap := &arr
arr0p := (*int)(unsafe.Pointer(ap))
arr1p := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(ap)) + unsafe.Sizeof(arr[0])))
*arr0p += 10
*arr1p += 20
fmt.Println(arr) // [11 22 3]
}
案例二:通过指针运算,修改结构体内的值。
package main
import (
"fmt"
"unsafe"
)
type user struct {
name string
age int
}
func main() {
u := new(user)
fmt.Println(*u) // { 0}
pName := (*string)(unsafe.Pointer(u))
*pName = "张三"
pAge := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(u)) + unsafe.Offsetof(u.age)))
*pAge = 20
fmt.Println(*u) // {张三 20}
}
通过以上案例,unsafe.Pointer绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。具体使用场景如下:
作为不同类型指针互相转换的中介;
利用 uintptr 突破指针不能进行算术运算的限制,从而达到直接操作内存的目的。
但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。
|希望能帮助你学习到新的知识点,欢迎提出宝贵的意见!!