简介
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 //1
age uint8 //1
min int // 8
name string // 16
slice []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 =16
fmt.Println(unsafe.Sizeof(h.slice)) //24 切片是个结构体,内含 array unsafe.Pointer len int cap int 8+8+8= 24
fmt.Println(unsafe.Offsetof(h.sex)) // 0 sex是第一个元素,偏移量为0
fmt.Println(unsafe.Offsetof(h.age)) // 1 age 前有一个bool类型,因此偏移量为1
fmt.Println(unsafe.Offsetof(h.min)) // 8 min前只有一个bool类型与一个uint8类型,但因为内存对齐,因此它的偏移量为8
fmt.Println(unsafe.Offsetof(h.name)) // 16
fmt.Println(unsafe.Offsetof(h.slice)) // 32
}
type Persom struct {
sex bool //1
age uint8 //1
min int // 8
name string // 16
slice []int8 // 24
s *School // 指针8
}
type School struct {
name string
addr 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 //1
age uint8 //1
min int // 8
name string // 16
slice []int8 // 24
s School // 两个string = 8*2
}
type School struct {
name string
addr 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 //1
age uint8 //1
min int // 8
name string // 16
slice []int8 // 24
s 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 //1
age uint8 //1
min int // 8
}
type Person2 struct {
sex bool //1
min int // 8
age uint8 //1
}
func main() {
p1 := Person1{
true,
30,
1,
}
p2 := Person2{
true,
30,
1,
}
fmt.Println(unsafe.Sizeof(p1)) // 16
fmt.Println(unsafe.Sizeof(p2)) // 24
}
上诉例子中,p2比p1类型多8个字节,就是因为内存对齐导致的,p2有两个内存空洞占用14个字节,而p1只有一个空洞,占6个字节
内存对齐原则
内存对齐:是按最大一个基础类型去对齐
内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的
// 最大类型是int32,因此按4字节做对齐
type Persom struct {
sex bool
a int32
b int32
}
// 最大类型是[]int8,但切片是个结构体
// 切片的数据结构: array unsafe.Pointer len int cap int
// 在64位操作系统中 unsafe.Pointer int int 都是占8字节
// 因此按8字节做对齐
type Persom struct {
sex bool
slice []int8
}
通过指针取值
通过指针取值,但并不建议这么操作
package main
import (
"fmt"
"unsafe"
)
type Persom struct {
sex bool
age uint8
min int
name 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 9
var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
fmt.Println(Cap, cap(s)) // 20 20
}
发现没问题
获取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
结构体互相转换
注意一定要满足以下情况:
- 结构体内字段顺序一致
- 结构体内字段类型一致 ```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} }
来个结构体嵌套
```go
type AAAA1 struct {
A1 string
}
type AAAA2 struct {
A2 string
}
type AAAA3 struct {
AAAA1
A3 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 string
A2 int64
A3 int64
}
type BBB struct {
B1 string
B2 int64
B3 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 uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap 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))
}
原理上是利用指针的强转,代码比较简单,不作详细解释。