unsafe黑魔法

前言

大家应该都知道 Go 语言是不支持指针的运算和转换的。这是为什么呢?

由于 Go 语言是一门静态语言(也叫强类型语言),所以所有的变量都必须为标量类型。不同类型之间不能够进行赋值、计算等跨类型的操作,比方说一个字符串不能赋值给一个 int 类型的变量,一个字符串也不能跟一个整数相加、相减等操作,一个字符串也不能强制转换为整数类型。由于指针也有其对应的类型,所以也在 Compile 的静态类型检查的范围内。同时,静态类型又叫强类型,意思就是这个变量一旦定义其类型,便不能再更改它。

像下面这种,就是不对的

number := 5
numberPointer := &number
floatNumber := (*float32)(numberPointer)
fmt.Println(floatNumber)

编译器会报错:

# command-line-arguments
...: cannot convert numberPointer (type *int) to type *float32

直接翻译就知道这是为什么会报错吧

unsafe 包

用它之前,我们要知道:

  • 它是围绕 Go 程序内存安全及类型的操作
  • 用它写出来的程序很可能是不可移植的
  • 不受 Go 1兼容性指南保护

unsafe.Pointer

type ArbitraryType int
type Pointer *ArbitraryType

ArbitraryType 仅用于来表示任意 Go 表达式类型,就是用来表示任意的含义,他并不属于 unsafe 包的一部分。

所以 Pointer 代表人意类型的指针,类似于 C 语言里面的 void*。

Pointer 有四个核心操作:

  • 任何类型的指针值都可以转换为 Pointer
  • Pointer 可以转换为任何类型的指针值
  • uintptr 可以转换为 Pointer
  • Pointer 可以转换为 uintptr
number := 5
numberPointer := &number
floatNumber := (*float32)(unsafe.Pointer(numberPointer))
fmt.Println(floatNumber)

这样写就不报错了。拿 unsafe.Pointer 作为中间的桥梁,巧妙地将 int 类型的 number 转换为 float32 类型的 floatNumber.

Offsetof(偏移量)

type N struct{
    i string
    j int
}

func main(){
    n := N{
        i: "example",
        j: 1
    }
    niPointer := unsafe.Pointer(&n)
    *niPointer = "鱼"
    njPointer := (*int)(unsafe.Pointer(uintptr(nPointer) + unsafe.Offsetof(n.j)))
    *njPointer = 2
    fmt.Printf("n.i: %s, n.j: %d", n.i, n.j)
}

输出结果:

n.i: 鱼, n.j: 2

分析这段代码做了什么事之前,我们需要知道关于结构体的一些概念:

  • 结构体成员变量在内存存储上是一块连续的内存
  • 结构体的初始地址就是第一个成员变量的内存地址
  • 基于结构体的成员地址去计算偏移量,就能够计算出其他成员变量的内存地址

关于 unitptr :

  • 是 Go 的内置类型。返回无符号整数,可存储一个完整的地址。
  type uintptr uinptr

关于 offsetof

func Offsetof(x ArbitraryType) uintptr
  • 计算偏移量大小,传入任意类型参数,返回返回x表示的字段结构中的偏移量,该字符必须是 structValue.field 形式,换句话说,它返回struct 的开头字段的开头之间的字节数。

这个示例就是先将整个结构体的 unsafe.Pointer 转换为 unsafe.uintptr 然后将得到的 unsafe.uintptr 加上某个字段的偏移量 转换成 该字段其他类型的指针值

从而巧妙地将结构体某个字段 转换为 其他类型的指针值

可以知道 uintptr 类型是可以做指针运算的

注意

uintptr 类型是不能存储在临时变量中的。因为从 GC 角度来看,uintptr 类型的临时变量只是一个无符号整数,并不知道它是一个指针地址。因此当满足一定条件后,这个临时变量很可能会被垃圾回收掉,那么接下来的内存操作岂不成谜?

总结

1⃣️ unsafe.Pointer 可以让你的变量在不同的指针类型之间转来转去,就是可以表示成任何可循知道指针类型。

2⃣️ uintptr 常用于与 unsafe.Pointer 互相配合做指针运算。

3⃣️ unsafe 包实现了 Go 语言做指针运算和指针类型之间转换