Go语言类型系统是为了保证安全和效率设计的,但保证安全的同时也会使程序效率低下。unsafe包可以绕过Go语言的类型系统限制,直接对内存进行读写操作。例如我们通常不能操作一个未导出的变量,但是通过unsafe包就可以做到。
unsafe包提供了三个函数,它们都作用于编译期间:
1
2
3
func Sizeof(x ArbitraryType) uintptr //返回类型所占用的字节数(非指向内容的大小)。
func AlignOf(x ArbitraryType) uintptr //返回一个值在内存中的地址对齐值
func Offsetof(x ArbitraryType) uintptr //返回结构体的某个成员地址相对于此结构体起始处的字节数,即地址偏移。
指针
unsafe.Pointer 有两个重要的能力:
- unsafe.Pointer 可以和 任何类型的指针相互转换。
- unsafe.Pointer 可以和 uintptr 类型相互转换。
需要注意Go语言中指针有几个限制:
- Go 的指针不能进行数学运算。
- 不同类型的指针不能相互转换。
- 不同类型的指针不能使用 == 或 != 比较。
uintptr 是Golang的内置类型,是用于存储指针的整型。uintptr 可以进行数学运算。
unsafe.Pointer 可以转换为 uintptr。那么结合使用 uintptr 和 unsafe.Pointer 就可以解决 Go 指针不能进行数学运算的限制。
需要注意:使用uintptr操作指针数据是危险的,因为不能保证该地址的内存块没有被回收、或已被重新分配。
修改结构体成员值
对于一个结构体,通过 unsafe.Offset 函数可以通过获取结构体成员的偏移量,进而获取到结构体成员的地址,读写该地址的内存,也就可以修改成员值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main
import (
"fmt"
"unsafe"
)
type Person struct {
name string
age int
}
func main() {
p := Person{
name: "hi",
age: 18,
}
fmt.Println(unsafe.Sizeof(p)) //24
fmt.Println(unsafe.Alignof(p)) //8
fmt.Println(unsafe.Offsetof(p.name)) //0
fmt.Println(unsafe.Offsetof(p.age)) //16
// 修改未导出变量 name
name := (*string)(unsafe.Pointer(&p))
*name = "test"
fmt.Printf("%+v\n", p) //{name:test age:18}
// 修改未导出变量 age
age := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.age)))
*age = 11
fmt.Printf("%+v\n", p) //{name:test age:11}
}
string 和 slice 转换
利用unsafe包可以做到字符串和bytes切片零拷贝的转换。这里需要先了解下slice和string的数据结构:
1
2
3
4
5
6
7
8
9
10
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
代码实现比较简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
import (
"fmt"
"reflect"
"unsafe"
)
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
sh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&sh))
}
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))
}
func main() {
s := "abc"
b := string2bytes(s)
fmt.Println(b)
s1 := bytes2string(b)
fmt.Println(s1)
}
使用 unsafe 包可以直接操作内存而绕过 Go 的类型系统限制。Go 语言的源码中大量使用了 unsafe 包,某些场景下使用 unsafe 包中的函数会提升代码的执行效率,但是通过包的名称可以看出来它是不安全的,使用它会有一定的风险,如果对它不是太了解的话最好还是尽量避免使用。