uintptr是 Go 语言的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是 uint64。
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
typedef unsigned long long int uint64;
typedef uint64 uintptr;
随着程序执行的进行,当前goroutine会经常发生栈扩容或者栈缩容,会把旧栈内存的数据拷贝到新栈区然后更改所有指针的指向。一个unsafe.Pointer是一个指针,因此当它指向的数据被移动到新栈区后指针也会被更新。但是uintptr类型的临时变量只是一个普通的数字,所以其值不会该被改变。上面错误的代码因为引入一个非指针的临时变量 tmp,导致系统无法正确识别这个是一个指向变量 x 的指针。当第二个语句执行时,变量 x 的数据可能已经被转移,这时候临时变量tmp也就不再是现在的 &x.b 的地址。第三个语句向之前无效地址空间的赋值语句将让整个程序崩溃。
unsafe 包用于编译阶段可以绕过 Go 语言的类型系统,直接操作内存。例如,利用 unsafe 包操作一个结构体的未导出成员。unsafe 包让我可以直接读写内存的能力。
unsafe包只有两个类型,三个函数,但是功能很强大。
ArbitraryType
是int
的一个别名,在Go中ArbitraryType
有特殊的意义。代表一个任意Go
表达式类型。Pointer
是int
指针类型的一个别名,在Go中可以把任意指针类型转换成unsafe.Pointer
类型。三个函数的参数均是
ArbitraryType
类型,就是接受任何类型的变量。Sizeof
接受任意类型的值(表达式),返回其占用的字节数,这和c语言里面不同,c语言里面sizeof函数的参数是类型,而这里是一个值,比如一个变量。Offsetof
:返回结构体成员在内存中的位置距离结构体起始处的字节数,所传参数必须是结构体的成员(结构体指针指向的地址就是结构体起始处的地址,即第一个成员的内存地址)。struct
类型,且还不能直接将这个struct
类型的变量当作参数,只能将这个struct
类型变量的属性当作参数。注意以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行
unsafe.Pointer
unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述:
unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void类型的指针),在Go 语言中是用于各种指针相互转换的桥梁,它可以持有任意类型变量的地址。
什么叫"可以持有任意类型变量的地址"呢?意思就是使用 unsafe.Pointer 转换的变量,该变量一定要是指针类型,否则编译会报错。
和普通指针一样,unsafe.Pointer 指针也是可以比较的,并且支持和
nil
比较判断是否为空指针。unsafe.Pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 unsafe.Pointer 类型。
uintptr
uintptr是 Go 语言的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是 uint64。
一个
unsafe.Pointer
指针也可以被转化为uintptr
类型,然后保存到uintptr
类型的变量中(注:这个变量只是和当前指针有相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是将uintptr转为unsafe.Pointer指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址。还有一点要注意的是,uintptr 并没有指针的语义,意思就是存储 uintptr 值的内存地址在Go发生GC时会被回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
使用unsafe.Pointer进行指针类型转换
使用unsafe.Pointer 读写结构体的私有成员
通过 Offsetof 方法可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。
这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。
上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性
如果改为下面这种用法是有风险的:
随着程序执行的进行,当前goroutine会经常发生栈扩容或者栈缩容,会把旧栈内存的数据拷贝到新栈区然后更改所有指针的指向。一个unsafe.Pointer是一个指针,因此当它指向的数据被移动到新栈区后指针也会被更新。但是uintptr类型的临时变量只是一个普通的数字,所以其值不会该被改变。上面错误的代码因为引入一个非指针的临时变量 tmp,导致系统无法正确识别这个是一个指向变量 x 的指针。当第二个语句执行时,变量 x 的数据可能已经被转移,这时候临时变量tmp也就不再是现在的 &x.b 的地址。第三个语句向之前无效地址空间的赋值语句将让整个程序崩溃。
string 和 []byte 零拷贝转换
这是一个非常精典的例子。实现字符串和 bytes 切片之间的零拷贝转换。
string和[]byte 在运行时的类型表示为
reflect.StringHeader
和reflect.SliceHeader
只需要共享底层 []byte 数组就可以实现零拷贝转换。
代码比较简单,不作详细解释。通过构造
reflect.StringHeader
和reflect.SliceHeader
,来完成 string 和 []byte 之间的转换。