简介

unsafe包提供了一些跳过go语言类型安全限制的操作。

  • 任何类型的指针都可以被转化为Pointer
  • Pointer可以被转化为任何类型的指针
  • uintptr可以被转化为Pointer
  • Pointer可以被转化为uintptr

举栗子:

  1. v1 := uint(1)
  2. v2 := int(2)
  3. p := &v1 // p的类型是(uint *)
  4. p = &v2 // 会报错,不能把(int *) 赋给(uint *)
  5. // 可以通过unsafe.Pointer实现
  6. 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例子

  1. type Persom struct {
  2. sex bool //1
  3. age uint8 //1
  4. min int // 8
  5. name string // 16
  6. slice []int // 24
  7. }
  8. func main() {
  9. h := Persom{
  10. true,
  11. 30,
  12. 1,
  13. "hello",
  14. make([]int, 0, 100),
  15. }
  16. fmt.Println(unsafe.Sizeof(h)) //56 h类型占用56个字节
  17. fmt.Println(unsafe.Sizeof(h.sex)) //1 bool 占1个字节
  18. fmt.Println(unsafe.Sizeof(h.age)) //1 uint8占1个字节
  19. fmt.Println(unsafe.Sizeof(h.min)) //8 int在64位系统中占8个字节
  20. fmt.Println(unsafe.Sizeof(h.name)) //16 string是个结构体,内含Data uintptr Len int, 8+8 =16
  21. fmt.Println(unsafe.Sizeof(h.slice)) //24 切片是个结构体,内含 array unsafe.Pointer len int cap int 8+8+8= 24
  22. fmt.Println(unsafe.Offsetof(h.sex)) // 0 sex是第一个元素,偏移量为0
  23. fmt.Println(unsafe.Offsetof(h.age)) // 1 age 前有一个bool类型,因此偏移量为1
  24. fmt.Println(unsafe.Offsetof(h.min)) // 8 min前只有一个bool类型与一个uint8类型,但因为内存对齐,因此它的偏移量为8
  25. fmt.Println(unsafe.Offsetof(h.name)) // 16
  26. fmt.Println(unsafe.Offsetof(h.slice)) // 32
  27. }
  1. type Persom struct {
  2. sex bool //1
  3. age uint8 //1
  4. min int // 8
  5. name string // 16
  6. slice []int8 // 24
  7. s *School // 指针8
  8. }
  9. type School struct {
  10. name string
  11. addr string
  12. }
  13. func main() {
  14. h := Persom{
  15. true,
  16. 30,
  17. 1,
  18. "hello",
  19. make([]int8, 0, 100),
  20. &School{},
  21. }
  22. fmt.Println(unsafe.Sizeof(h)) //64
  23. }
  1. type Persom struct {
  2. sex bool //1
  3. age uint8 //1
  4. min int // 8
  5. name string // 16
  6. slice []int8 // 24
  7. s School // 两个string = 8*2
  8. }
  9. type School struct {
  10. name string
  11. addr string
  12. }
  13. func main() {
  14. h := Persom{
  15. true,
  16. 30,
  17. 1,
  18. "hello",
  19. make([]int8, 0, 100),
  20. &School{},
  21. }
  22. fmt.Println(unsafe.Sizeof(h)) //88
  23. }
  1. type Persom struct {
  2. sex bool //1
  3. age uint8 //1
  4. min int // 8
  5. name string // 16
  6. slice []int8 // 24
  7. s map[int64]int64 // map是一个指针类型
  8. c chan int64 //chan是一个指针类型
  9. }
  10. func main() {
  11. h := Persom{
  12. true,
  13. 30,
  14. 1,
  15. "hello",
  16. make([]int8, 0, 100),
  17. nil,
  18. nil,
  19. }
  20. fmt.Println(unsafe.Sizeof(h)) //56 h类型占用56个字节
  21. }

内存对齐

  1. type Person1 struct {
  2. sex bool //1
  3. age uint8 //1
  4. min int // 8
  5. }
  6. type Person2 struct {
  7. sex bool //1
  8. min int // 8
  9. age uint8 //1
  10. }
  11. func main() {
  12. p1 := Person1{
  13. true,
  14. 30,
  15. 1,
  16. }
  17. p2 := Person2{
  18. true,
  19. 30,
  20. 1,
  21. }
  22. fmt.Println(unsafe.Sizeof(p1)) // 16
  23. fmt.Println(unsafe.Sizeof(p2)) // 24
  24. }

上诉例子中,p2比p1类型多8个字节,就是因为内存对齐导致的,p2有两个内存空洞占用14个字节,而p1只有一个空洞,占6个字节

内存对齐原则

内存对齐:是按最大一个基础类型去对齐
内存对齐最最底层的原因是内存的IO是以8个字节64bit为单位进行的

  1. // 最大类型是int32,因此按4字节做对齐
  2. type Persom struct {
  3. sex bool
  4. a int32
  5. b int32
  6. }
  7. // 最大类型是[]int8,但切片是个结构体
  8. // 切片的数据结构: array unsafe.Pointer len int cap int
  9. // 在64位操作系统中 unsafe.Pointer int int 都是占8字节
  10. // 因此按8字节做对齐
  11. type Persom struct {
  12. sex bool
  13. slice []int8
  14. }

通过指针取值

通过指针取值,但并不建议这么操作

  1. package main
  2. import (
  3. "fmt"
  4. "unsafe"
  5. )
  6. type Persom struct {
  7. sex bool
  8. age uint8
  9. min int
  10. name string
  11. }
  12. func main() {
  13. h := &Persom{
  14. true,
  15. 20,
  16. 1,
  17. "hello",
  18. }
  19. sex := *(*bool)(unsafe.Pointer(uintptr(unsafe.Pointer(h))))
  20. age := *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(h))+1))
  21. //age := *(*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(h))+unsafe.Offsetof(h.age)))
  22. min := *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(h))+8))
  23. name := *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(h))+16))
  24. fmt.Println(sex,age,min,name)
  25. }

获取 slice 长度

不通过len函数,如何获取slice的长度?
slice header 的结构体定义为

  1. type slice struct {
  2. array unsafe.Pointer // 元素指针
  3. len int // 长度
  4. cap int // 容量
  5. }

尝试使用unsafe来对slice中的len与cap取值

  1. func main() {
  2. s := make([]int, 9, 20)
  3. var Len = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(8)))
  4. fmt.Println(Len, len(s)) // 9 9
  5. var Cap = *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + uintptr(16)))
  6. fmt.Println(Cap, cap(s)) // 20 20
  7. }
  8. 发现没问题

获取map的长度

map底层内部结构如下

  1. type hmap struct {
  2. count int
  3. flags uint8
  4. B uint8
  5. noverflow uint16
  6. hash0 uint32
  7. buckets unsafe.Pointer
  8. oldbuckets unsafe.Pointer
  9. nevacuate uintptr
  10. extra *mapextra
  11. }
  12. // 和 slice 不同的是,makemap 函数返回的是 hmap 的指针,注意是指针:
  13. func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap

我们依然能通过 unsafe.Pointer 和 uintptr 进行转换,得到 hamp 字段的值,只不过,现在 count 变成二级指针了:

  1. func main() {
  2. mp := make(map[string]int)
  3. mp["qcrao"] = 100
  4. mp["stefno"] = 18
  5. count := **(**int)(unsafe.Pointer(&mp))
  6. fmt.Println(count, len(mp)) // 2 2
  7. }
  8. count 的转换过程:
  9. &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} }

  1. 来个结构体嵌套
  2. ```go
  3. type AAAA1 struct {
  4. A1 string
  5. }
  6. type AAAA2 struct {
  7. A2 string
  8. }
  9. type AAAA3 struct {
  10. AAAA1
  11. A3 string
  12. }
  13. type AAAA4 struct {
  14. A2 AAAA2 // 隐式与显示都可以,隐式的结果 {AAAA2:{A2:a1} A4:a3}
  15. A4 string
  16. }
  17. func main() {
  18. AAA.Run()
  19. a1 := AAAA3{AAAA1{"a1"},"a3"}
  20. a2 := *(*AAAA4)(unsafe.Pointer(&a1))
  21. fmt.Printf("%+v",a2) //{A2:{A2:a1} A4:a3}
  22. }

来个错误例子

  1. type AAA struct {
  2. A1 string
  3. A2 int64
  4. A3 int64
  5. }
  6. type BBB struct {
  7. B1 string
  8. B2 int64
  9. B3 string
  10. }
  11. func main() {
  12. A1 := AAA{
  13. "12345",
  14. 100,
  15. 1000,
  16. }
  17. B1 := *(*BBB)(unsafe.Pointer(&A1))
  18. fmt.Println(B1)
  19. }

image.png

如何实现字符串和byte切片的零拷贝转换

这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。
完成这个任务,我们需要了解 slice 和 string 的底层数据结构:

  1. type StringHeader struct {
  2. Data uintptr
  3. Len int
  4. }
  5. type SliceHeader struct {
  6. Data uintptr
  7. Len int
  8. Cap int
  9. }

string 里的Data 指向的是底层byte数组
[]byte 里的Data 指向的是底层byte数组
因此只需要共享底层 Data 和 Len 就可以实现 zero-copy
上面是反射包下的结构体,路径:src/reflect/value.go。

  1. func string2bytes(s string) []byte {
  2. return *(*[]byte)(unsafe.Pointer(&s))
  3. }
  4. func bytes2string(b []byte) string{
  5. return *(*string)(unsafe.Pointer(&b))
  6. }

原理上是利用指针的强转,代码比较简单,不作详细解释。