类型的指针和 unsafe.Pointer 可以相互转换。
uintptr 类型和 unsafe.Pointer 可以相互转换。
pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。
// uintptr 是一个整数类型,它足够大,可以存储
type uintptr uintptr
还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。
unsafe 包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 /usr/local/go/src/cmd/compile/internal/gc/unsafe.go
路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。
更深层的原理需要去研究编译器的源码,这里就不去深究了。我们重点关注它的用法,接着往下看。
unsafe 如何使用
获取 slice 长度
通过前面关于 slice 的文章,我们知道了 slice header 的结构体定义:
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
调用 make 函数新建一个 slice,底层调用的是 makeslice 函数,返回的是 slice 结构体:
func makeslice(et *_type, len, cap int) slice
因此我们可以通过 unsafe.Pointer 和 uintptr 进行转换,得到 slice 的字段值。
func main() {
s := make([]int, 9, 20)
var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))
fmt.Println(Len, len(s)) // 9 9
var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
fmt.Println(Cap, cap(s)) // 20 20
}
Len,cap 的转换流程如下:
Len: &s => pointer => uintptr => poiter => *int => int
Cap: &s => pointer => uintptr => poiter => *int => int
获取 map 长度
再来看一下上篇文章我们讲到的 map:
type hmap struct {
count int
flags uint8
B uint8
noverflow uint16
hash0 uint32
buckets unsafe.Pointer
oldbuckets unsafe.Pointer
nevacuate uintptr
extra *mapextra
}
和 slice 不同的是,makemap 函数返回的是 hmap 的指针,注意是指针:
func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap
我们依然能通过 unsafe.Pointer 和 uintptr 进行转换,得到 hamp 字段的值,只不过,现在 count 变成二级指针了:
func main() {
mp := make(map[string]int)
mp["qcrao"] = 100
mp["stefno"] = 18
count := **(**int)(unsafe.Pointer(&mp))
fmt.Println(count, len(mp)) // 2 2
}
count 的转换过程:
&mp => pointer => **int => int
map 源码中的应用
在 map 源码中,mapaccess1、mapassign、mapdelete 函数中,需要定位 key 的位置,会先对 key 做哈希运算。
例如:
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))
h.buckets
是一个 unsafe.Pointer
,将它转换成 uintptr
,然后加上 (hash&m)*uintptr(t.bucketsize)
,二者相加的结果再次转换成 unsafe.Pointer
,最后,转换成 bmap 指针
,得到 key 所落入的 bucket 位置。如果不熟悉这个公式,可以看看上一篇文章,浅显易懂。
上面举的例子相对简单,来看一个关于赋值的更难一点的例子:
// store new key/value at insert position
if t.indirectkey {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectvalue {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(val) = vmem
}
typedmemmove(t.key, insertk, key)
这段代码是在找到了 key 要插入的位置后,进行“赋值”操作。insertk 和 val 分别表示 key 和 value 所要“放置”的地址。如果 t.indirectkey 为真,说明 bucket 中存储的是 key 的指针,因此需要将 insertk 看成指针的指针
,这样才能将 bucket 中的相应位置的值设置成指向真实 key 的地址值,也就是说 key 存放的是指针。
下面这张图展示了设置 key 的全部操作:
obj 是真实的 key 存放的地方。第 4 号图,obj 表示执行完 typedmemmove
函数后,被成功赋值。
Offsetof 获取成员偏移量
对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。
这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。
我们来看一个例子:
package main
import (
"fmt"
"unsafe"
)
type Programmer struct {
name string
language string
}
func main() {
p := Programmer{"stefno", "go"}
fmt.Println(p)
name := (*string)(unsafe.Pointer(&p))
*name = "qcrao"
lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language))