Go unsafe 包探究

unsafe 包的作用有两个:

  • 实现任意不同类型指针之间的转换;
  • 实现指针运算(偏移)操作;

包接口比较简单,包括 3 个函数:

  • func Alignof(x ArbitraryType) uintptr 变量对齐;
  • func Offsetof(x ArbitraryType) uintptr 计算结构体(struct)内属性值的偏移字节大小,即相对结构体起始地址的大小;
  • func Sizeof(x ArbitraryType) uintptr 类型变量自身占用字节大小,注意,不包括变量引用的地址;

unsafe 函数都是在编译时计算返回结果的,所以,可以直接用于常量赋值,也要注意,尽量不要将运行时变量类型(例如 slice)作为这些函数的参数出入,可能会导致非预期的结果。

包括 2 种类型:

  • type ArbitraryType 占位符,实际上表示任意的变量类型;
  • type Pointer 指向任意类型的指针类型。类似 C 中 void * 类型。

unsafe.Pointer 是本包的精华,也是被使用最多的功能点。Pointer 允许程序(开发者)跳脱 Go 的类型系统,(通过指针转换与运算)读写任意内存,所以要小心使用。 Pointer 总结下来就两个特性,也是实现前面说的目标(作用)的基础:

  • Pointer 能与任意类型的指针值互相转换;
  • Pointer 能与 uintpter 相互转换;

uintptr 是无符号整型,被用来存放指针值(地址)。unsafe.Pointer + uintptr 就能实现指针偏移计算了。因为 uintptr 变量存放的是某个变量的地址,因此,uintpter 变量值对应的内存地址块(对应的变量)可能会被 GC 回收掉。unsafe.Pointer 本质上就是指针,该类型变量指向的内存块则不会被回收,因此,应该使用 unsafe.Pointer 类型变量来保持变量地址不被回收。

// 不安全的使用

z := uintptr(unsafe.Pointer(&xx))
//todo ...
fmt.Println(z)

//正确使用

sp:=safe.Pointer(&xx)
z = uintptr(sp)
//todo ...
fmt.Println(z)

安全的使用场景

前面说过,使用 Pointer 必须要非常小心才行,官方定义了 6 种安全有效的使用场景。使用 go vet 工具可以检测出不符合这些场景的调用。

  • 将指针 *T1 转化成 *T2

如果 T2 大于 T1 变量类型的内存占用,并且两者共享等效的内存布局,则该转换允许将一种类型数据解释成为另一种类型。例如:int64 与 float64。

  • 将 Pointer 转成 uintptr,但是,不能转回到 Pointer

与 Pointer 不同,uintptr 保存的地址指向的变量是可以被 GC 回收的。

可以使用 runtime.KeepAlive 函数避免变量被 GC。

  • 将 Pointer 转成 uintptr,用于偏移运算,将计算结果转回成 Pointer

通常用于访问结构体或者数组等:

// equivalent to f := unsafe.Pointer(&s.f)
f := unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f))

// equivalent to e := unsafe.Pointer(&x[i])
e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))

特别注意:因为上面说的原因,uintptr 不能放在临时变量内,所以下面这样分开使用也是无效的

u := uintptr(p)
p = unsafe.Pointer(u + offset)

另外,做地址偏移的时候,要注意越界的问题,比如:

a = []int{0,1,2,3}
p := unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + len(a) * unsafe.Sizeif(a[0]))

此时,变量 p 指向的地址是未知的,可能会出现不声明,直接偷偷的读写未知内存地址的情况,对系统运行稳定性影响很大。

  • 使用函数 syscall.Syscall 时,将 Pointer 值转成 uintptr

syscall.Syscall(SYS_READ, uintptr(fd), uintptr(unsafe.Pointer(p)), uintptr(n))

  • 将函数 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 返回值,从 uintptr 转成 Pointer,最终转成具体的类型值
p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer()))
  • 将 reflect.SliceHeader 或 reflect.StringHeader 的数据字段(Data)转成 Pointer,或者从 Pointer 转成 Data 字段

Data 字段返回的也是 uintptr:

var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p))              // case 6 (this case)
hdr.Len = n

reflect.SliceHeader 的使用:

package main

import "fmt"
import "unsafe"
import "reflect"
import "runtime"

func main() {
        bs := []byte("Golang")
        var pa *[2]byte // an array pointer
        hdr := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
        pa = (*[2]byte)(unsafe.Pointer(hdr.Data))
        runtime.KeepAlive(&bs)
        fmt.Printf("%s\n", pa) // &Go
        pa[1] = 'a'
        fmt.Printf("%s\n", bs) // Galang
}

如果最后一行的 Printf 不存在的话,runtime.KeepAlive 的调用是必须的。 另外,最好不要像下面这样,直接从 StringHeader 或 StringHeader 直接创建对象:

// Assume p points to a sequence of byte and
// n is the number of bytes in the sequence.
var hdr reflect.StringHeader
hdr.Data = uintptr(unsafe.Pointer(new([5]byte)))
// Now the just allocated byte array has lose all
// references and it can be garbage collected now.
hdr.Len = 5
s := *(*string)(unsafe.Pointer(&hdr))

小结

在有些场景下,使用 unsafe.Poniter 可以帮助我们写出高效的代码,例如在 sync/atomic 包内的使用。而且一些底层或 C 调用,必须要用到 Poniter。

unsafe 包用于有经验的开发者绕过 Go 类型系统的安全性限制,一定要深入理解上面的六种使用场景,谨慎使用,否则很容易引起严重的内存问题,有经验的开发者都知道,这类问题通常是很难定位的,对系统的稳定性影响很大。

参考

Go 101 : https://go101.org/article/unsafe.html

更多技术文章分享

1 个评论

说得好,可惜用了unsafe包的pr都不让过

要回复文章请先登录注册