Go语言大厂编程 unsafe 不安全指针

Go指针和unsafe.Pointer有什么区别

  • Go 的指针不能进行数学运算
  • 不同类型的指针不能相互转换
  • 不同类型的指针不能使用 == 或 != 比较
  • 不同类型的指针变量不能相互赋值

unsafe 包提供了 2 点重要的能力:

unsafe pointer.png

pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。

// uintptr 是一个整数类型,它足够大,可以存储
type uintptr uintptr
复制代码

还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。

unsafe 包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 unsafe.go 路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。

如何利用unsafe包修改私有成员

对于一个结构体,通过 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"
    //unsafe.Offsetof 返回结构开头和字段开头之间的字节数
    lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.language)))
    *lang = "Golang"
    fmt.Println(p)
}
复制代码

运行代码,输出:

{stefno go}
{qcrao Golang}
复制代码

name 是结构体的第一个成员,因此可以直接将 &p 解析成 *string。 对于结构体的私有成员,现在有办法可以通过 unsafe.Pointer 改变它的值了。 我把 Programmer 结构体升级,多加一个字段:

type Programmer struct {
    name string
    age int
    language string
}
复制代码

并且放在其他包,这样在 main 函数中,它的三个字段都是私有成员变量,不能直接修改。但我通过 unsafe.Sizeof() 函数可以获取成员大小,进而计算出成员的地址,直接修改内存。

func main() {
    p := Programmer{"stefno", 18, "go"}
    fmt.Println(p)
    lang := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Sizeof(int(0)) + unsafe.Sizeof(string(""))))
    *lang = "Golang"
    fmt.Println(p)
}
复制代码

这里仅仅是对如何设置私有变量的方法进行扩展说明,在当前场景下,unsafe.Offsetof(p.language)能够替代unsafe.Sizeof(int(0)) + unsafe.Sizeof(string(""))

输出:

{stefno 18 go}
{stefno 18 Golang}
复制代码

字符串和byte切片的零拷贝转换

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

type StringHeader struct {
    Data uintptr
    Len  int
}
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
复制代码

只需要共享底层 []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))
}
复制代码

代码比较简单,不作详细解释。通过构造 slice header 和 string header,来完成 string 和 byte slice 之间的转换。

猜你喜欢

转载自juejin.im/post/7068825554606096415