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