简介
unsafe包提供了一些跳过go语言类型安全限制的操作。
- 任何类型的指针都可以被转化为Pointer
- Pointer可以被转化为任何类型的指针
- uintptr可以被转化为Pointer
- Pointer可以被转化为uintptr
举栗子:
v1 := uint(1)v2 := int(2)p := &v1 // p的类型是(uint *)p = &v2 // 会报错,不能把(int *) 赋给(uint *)// 可以通过unsafe.Pointer实现p = (*uint)(unsafe.Pointer(&v2))
type ArbitraryType int
ArbitraryType在本文档里表示任意一种类型
type Pointer *ArbitraryType
Pointer类型用于表示任意类型的指针。有4个特殊的只能用于Pointer类型的操作:
- 1) 任意类型的指针可以转换为一个Pointer类型值
- 2) 一个Pointer类型值可以转换为任意类型的指针
- 3) 一个uintptr类型值可以转换为一个Pointer类型值
- 4) 一个Pointer类型值可以转换为一个uintptr类型值
func Sizeof(v ArbitraryType) uintptr
- 该函数返回 参数类型 所占内存的字节数,注意不是参数所占内存的字节数
func Alignof(v ArbitraryType) uintptr
- 返回 m,m 是指当类型进行内存对齐时,它分配到的内存地址能整除 m
func Offsetof(v ArbitraryType) uintptr
- 返回结构体中某个field的偏移量,换句话说,它返回该结构起始处与该字段起始处之间的字节数
unsafe例子
type Persom struct {sex bool //1age uint8 //1min int // 8name string // 16slice []int // 24}func main() {h := Persom{true,30,1,"hello",make([]int, 0, 100),}fmt.Println(unsafe.Sizeof(h)) //56 h类型占用56个字节fmt.Println(unsafe.Sizeof(h.sex)) //1 bool 占1个字节fmt.Println(unsafe.Sizeof(h.age)) //1 uint8占1个字节fmt.Println(unsafe.Sizeof(h.min)) //8 int在64位系统中占8个字节fmt.Println(unsafe.Sizeof(h.name)) //16 string是个结构体,内含Data uintptr Len int, 8+8 =16fmt.Println(unsafe.Sizeof(h.slice)) //24 切片是个结构体,内含 array unsafe.Pointer len int cap int 8+8+8= 24fmt.Println(unsafe.Offsetof(h.sex)) // 0 sex是第一个元素,偏移量为0fmt.Println(unsafe.Offsetof(h.age)) // 1 age 前有一个bool类型,因此偏移量为1fmt.Println(unsafe.Offsetof(h.min)) // 8 min前只有一个bool类型与一个uint8类型,但因为内存对齐,因此它的偏移量为8fmt.Println(unsafe.Offsetof(h.name)) // 16fmt.Println(unsafe.Offsetof(h.slice)) // 32}
type Persom struct {sex bool //1age uint8 //1min int // 8name string // 16slice []int8 // 24s *School // 指针8}type School struct {name stringaddr string}func main() {h := Persom{true,30,1,"hello",make([]int8, 0, 100),&School{},}fmt.Println(unsafe.Sizeof(h)) //64}
type Persom struct {sex bool //1age uint8 //1min int // 8name string // 16slice []int8 // 24s School // 两个string = 8*2}type School struct {name stringaddr string}func main() {h := Persom{true,30,1,"hello",make([]int8, 0, 100),&School{},}fmt.Println(unsafe.Sizeof(h)) //88}
type Persom struct {sex bool //1age uint8 //1min int // 8name string // 16slice []int8 // 24s map[int64]int64 // map是一个指针类型c chan int64 //chan是一个指针类型}func main() {h := Persom{true,30,1,"hello",make([]int8, 0, 100),nil,nil,}fmt.Println(unsafe.Sizeof(h)) //56 h类型占用56个字节}
内存对齐
type Person1 struct {sex bool //1age uint8 //1min int // 8}type Person2 struct {sex bool //1min int // 8age uint8 //1}func main() {p1 := Person1{true,30,1,}p2 := Person2{true,30,1,}fmt.Println(unsafe.Sizeof(p1)) // 16fmt.Println(unsafe.Sizeof(p2)) // 24}
上诉例子中,p2比p1类型多8个字节,就是因为内存对齐导致的,p2有两个内存空洞占用14个字节,而p1只有一个空洞,占6个字节
内存对齐原则
内存对齐:是按最大一个基础类型去对齐
内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的
// 最大类型是int32,因此按4字节做对齐type Persom struct {sex boola int32b int32}// 最大类型是[]int8,但切片是个结构体// 切片的数据结构: array unsafe.Pointer len int cap int// 在64位操作系统中 unsafe.Pointer int int 都是占8字节// 因此按8字节做对齐type Persom struct {sex boolslice []int8}
通过指针取值
通过指针取值,但并不建议这么操作
package mainimport ("fmt""unsafe")type Persom struct {sex boolage uint8min intname string}func main() {h := &Persom{true,20,1,"hello",}sex := *(*bool)(unsafe.Pointer(uintptr(unsafe.Pointer(h))))age := *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(h))+1))//age := *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(h))+unsafe.Offsetof(h.age)))min := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(h))+8))name := *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(h))+16))fmt.Println(sex,age,min,name)}
获取 slice 长度
不通过len函数,如何获取slice的长度?
slice header 的结构体定义为
type slice struct {array unsafe.Pointer // 元素指针len int // 长度cap int // 容量}
尝试使用unsafe来对slice中的len与cap取值
func main() {s := make([]int, 9, 20)var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))fmt.Println(Len, len(s)) // 9 9var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))fmt.Println(Cap, cap(s)) // 20 20}发现没问题
获取map的长度
map底层内部结构如下
type hmap struct {count intflags uint8B uint8noverflow uint16hash0 uint32buckets unsafe.Pointeroldbuckets unsafe.Pointernevacuate uintptrextra *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"] = 100mp["stefno"] = 18count := **(**int)(unsafe.Pointer(&mp))fmt.Println(count, len(mp)) // 2 2}count 的转换过程:&mp => pointer => **int => int
结构体互相转换
注意一定要满足以下情况:
- 结构体内字段顺序一致
- 结构体内字段类型一致 ```go type AAA struct { A1 string A2 int64 }
type BBB struct { B1 string B2 int64 B3 int64 }
func main() { A := AAA{ “12345”, 100, } B := (*BBB)(unsafe.Pointer(&A)) fmt.Printf(“%+v”, B) // &{B1:12345 B2:100 B3:0} }
来个结构体嵌套```gotype AAAA1 struct {A1 string}type AAAA2 struct {A2 string}type AAAA3 struct {AAAA1A3 string}type AAAA4 struct {A2 AAAA2 // 隐式与显示都可以,隐式的结果 {AAAA2:{A2:a1} A4:a3}A4 string}func main() {AAA.Run()a1 := AAAA3{AAAA1{"a1"},"a3"}a2 := *(*AAAA4)(unsafe.Pointer(&a1))fmt.Printf("%+v",a2) //{A2:{A2:a1} A4:a3}}
来个错误例子
type AAA struct {A1 stringA2 int64A3 int64}type BBB struct {B1 stringB2 int64B3 string}func main() {A1 := AAA{"12345",100,1000,}B1 := *(*BBB)(unsafe.Pointer(&A1))fmt.Println(B1)}

如何实现字符串和byte切片的零拷贝转换
这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。
完成这个任务,我们需要了解 slice 和 string 的底层数据结构:
type StringHeader struct {Data uintptrLen int}type SliceHeader struct {Data uintptrLen intCap int}
string 里的Data 指向的是底层byte数组
[]byte 里的Data 指向的是底层byte数组
因此只需要共享底层 Data 和 Len 就可以实现 zero-copy
上面是反射包下的结构体,路径:src/reflect/value.go。
func string2bytes(s string) []byte {return *(*[]byte)(unsafe.Pointer(&s))}func bytes2string(b []byte) string{return *(*string)(unsafe.Pointer(&b))}
原理上是利用指针的强转,代码比较简单,不作详细解释。
