目录

go绝知—unsafe包

go中指针

先看下go中的指针,go中的指针与c语言中的指针有很大不同:

  • go指针不支持运算
  • 不同类型无法转换
  • 不同类型指针不可比较

我们不难看出,go中的指针更加安全,但是却失去了灵活性,而且有些通过指针高效的处理数据的能力也失去了。

unsafe包

unsafe包的出现,可以让我们更高效的处理数据,但是和他的名字一样,不安全!

我们可以利用unsafe包拿到结构体struct中的未导出字段。

使用unsafe

阅读unsafe包文档中列出的规则:

任何类型的指针值都可以转换为unsafe.Pointer。 unsafe.Pointer可以转换为任何类型的指针值。 uintptr可以转换为unsafe.Pointer。 unsafe.Pointer可以转换为uintptr。

简单而言就是:unsafe.Pointer可以与任何类型指针值转换,unintptr可以与unsafe.Pointer互转。

unsafe 包还有其他三个函数:

func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr

ArbitraryType是任意的意思。

获取slice长度

我们换种方式来取slice的长度

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

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
}

Offsetof 获取成员偏移量

对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。

这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。

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)))
    *lang = "Golang"

    fmt.Println(p)
}

这里name成员的地址就是结构体的地址。

string 和 slice 的相互转换

string和slice的转换直接强转即可。

func main() {
	str := "abc"
	var s []byte = []byte(str)
	fmt.Println(cap(s))

	str2 := string(s)
	fmt.Println(str2)
}

这里转换的时候是有内存重新分配转换的。他们底层虽然都是数组,但是string的底层数组是只读的,不能修改的,slice底层数组是可以修改的。

这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。

完成这个任务,我们需要了解 slice 和 string 的底层数据结构:

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

我们可以看到,string本质还是个结构体,只需要共享底层 []byte 数组就可以实现 zero-copy。

func string2bytes(s string) []byte {
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

    bh := reflect.SliceHeader{
        Data: stringHeader.Data,
        Len:  stringHeader.Len,
        Cap:  stringHeader.Len,
    }

    return *(*[]byte)(unsafe.Pointer(&bh))
}

func bytes2string(b []byte) string{
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))

    sh := reflect.StringHeader{
        Data: sliceHeader.Data,
        Len:  sliceHeader.Len,
    }

    return *(*string)(unsafe.Pointer(&sh))
}

总结

unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。