0%

《Golang》Unsafe包的用处

源码浅析

1
2
3
4
5
6
7
8
9
10
11
package unsafe

type ArbitraryType int // 表示Go中的任意类型,这里只是文档的作用,编译器会做处理(其实编译器会对这个包下的所有方法进行处理)

type Pointer *ArbitraryType

func Sizeof(x ArbitraryType) uintptr // 返回变量的类型占用的空间大小,但不是变量实际占用的空间大小。比如rune类型的变量得出的结果一定是4,但占用的空间可能只有1

func Offsetof(x ArbitraryType) uintptr // 返回struct中字段在struct的偏移量

func Alignof(x ArbitraryType) uintptr // 查看变量对齐方式。n一个字节n个字节地对齐,这里就返回n

指针寻址功能

在Golang源码中,非常多的地方用到了指针寻址,以为很多地方都是直接操作内存,当然要用到指针寻址

比如,想使用变量a的地址加上某个数字

&a+8这样是不能运算的,他们类型不一样

但是可以下面这样操作

1
2
3
4
5
6
func main() {
a := test.Test{}
addrInt := uintptr(unsafe.Pointer(&a)) + 8 // 这样,就得到了相加后的地址,但是int类型的,不是指针类型
ptr := unsafe.Pointer(addrInt) // 转化成unsafe.Pointer抽象类型
pb := (*int16)(ptr) // 再转化成具体的指针类型
}

类型转换功能

比如slice转换成string

slice结构体分别由指向底层数组指针、数组大小、容量组成,而string结构体分别由底层数组指针、数组大小组成

可以看到slice和string组成的前两个字段是一样的,那么如果一个指针指向结构体第一个元素,是不是同时可以代表slice和string呢,答案是可以的

看下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"unsafe"
)

func main() {
a := []byte{97}
b := *(*string)(unsafe.Pointer(&a)) // 指向a结构体的指针转化成unsafe.Pointer,然后强转成*string类型,然后读出string结构体
fmt.Println(b) // 输出字母a

b = string(a) // 上面和这里是一样的效果。实际上这里内部就是上面方式转换的
}

修改struct中没有被暴露的字段功能

这个可以做到,但是基本不会被使用,因为这样操作不会带来什么好处,反而违反常规且会造成很多人的困扰不理解

bin/main/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"create_golang_app_template/bin/main/test"
"fmt"
"unsafe"
)

func main() {
a := test.Test{}
fmt.Println(a) // { 0}
pb := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + unsafe.Sizeof(""))) // unsafe.Sizeof("")获得空字符串占用大小
fmt.Printf("%v\n", pb)
*pb = 1 // 修改b的内容
fmt.Println(a) // { 1}
}

bin/main/test/type.go

1
2
3
4
5
6
package test

type Test struct {
a string
b 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
package main

import (
"fmt"
"unsafe"
)

type Test struct {
a string
b int
}

//go:noinline
func test(c int) uintptr {
a := Test{
a: "123",
b: c,
}
p := unsafe.Pointer(&a) // 取出a的栈中指针
addrInt := uintptr(p) + unsafe.Sizeof(a.a) // 指针+string占用的大小=a.b的位置
return addrInt

}

func main() {
addrInt := test(23)
test(33)
//fmt.Println(11)
pb := (*int)(unsafe.Pointer(addrInt)) // a.b的位置转换成*int
fmt.Println(*pb) // 输出int值
}

先看看,你觉得会输出多少?

答案是33,为什么不是23?

因为被覆盖了。test函数禁用内敛,所以拥有自己的栈帧,而且test函数中a变量不存在逃逸,所以a完全分配在栈中,test函数结束a的栈帧就被清除

但是,test使用的栈帧中的数据并不会被清除,因为没必要,下个函数调用直接覆盖就行了

本例中,第二次调用了test函数,导致第一次调用使用的栈帧空间被第二次调用使用的栈帧完全覆盖,所以23被覆盖成了33,所以取出来的就是33

再想想,如果第二次调用后面的一行打印注视去掉,最后打印又是啥呢?

答案是:不确定。33会被覆盖,覆盖成什么就会打出什么

从例子中可以看出,unsafe.Pointer的使用还是要注意这种问题的,转换成uintptr后,uintptr指向的内存区域可能已经没了(在栈区被覆盖了或者在堆区被回收了)




微信关注我,及时接收最新技术文章