13.2. unsafe.Pointer

大多数指针类型写成 T, 含义是 "一个指向T类型变量的指针". unsafe.Pointer 是特别定义的一种指针类型, 它可以包含任意类型变量的地址. 当然, 我们不可以直接使用 p 获取 unsafe.Pointer 指针指向的真实变量, 因为我们并不知道变量的类型. 和普通指针一样, unsafe.Pointer 指针是可以比较的, 支持和 nil 比较判断是否为空指针.

一个普通的 T 类型指针可以被转化为 unsafe.Pointer 类型指针, 并且一个 unsafe.Pointer 类型指针也可以被转回普通指针, 也可以是和 T 不同类型的指针. 通过将 *float64 类型指针 转化为 *uint64 类型指针, 我们可以检查一个浮点数变量的位模式.

package math

func Float64bits(f float64) uint64 { return *(*uint64)(unsafe.Pointer(&f)) }

fmt.Printf("%#016x\n", Float64bits(1.0)) // "0x3ff0000000000000"

通过新指针, 我们可以更新浮点数的位模式. 通过位模式操作浮点数是可以的, 但是更重要的意义是指针转换让我们可以在不破坏类型系统的前提下向内存写入任意的值.

一个 unsafe.Pointer 指针也可以被转化为 uintptr 类似, 然后保存到指针型数值变量中, 用以做必要的指针运算. (第三章内容, uintptr是一个无符号的整型数, 足有保存一个地址.) 这种转换也是可逆的, 但是, 将 uintptr 转为 unsafe.Pointer 指针可能破坏类型系统, 因为并不是所有的数字都是有效的内存地址.

许多将 unsafe.Pointer 指针 转为原生数字, 然后再转为 unsafe.Pointer 指针的操作是不安全的. 下面的例子需要将变量 x 的地址加上 b 字段的偏移转化为 *int16 类型指针, 然后通过该指针更新 x.b:

//gopl.io/ch13/unsafeptr

var x struct {
    a bool
    b int16
    c []int
}

// 和 pb := &x.b 等价
pb := (*int16)(unsafe.Pointer(
    uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"

尽管写法很繁琐, 但在这里并不是一件坏事, 因为这些功能应该很谨慎地使用. 不要试图将引入可能而破坏代码的正确性的 uintptr 临时变量. 下面段代码是不正确的:

错误的原因很微妙. 有时候垃圾回收器会移动一些变量以降低内存碎片的问题.这类垃圾回收器被称为移动GC. 当一个变量被移动, 所有的保存改变量旧地址的指针必须同时被更新为变量移动后的新地址. 从垃圾收集器的视角来看, 一个 unsafe.Pointer 是一个指针, 因此当变量被移动是对应的指针必须被更新, 但是 uintptr 只是一个普通的数字, 所以其值不应该被改变. 上面错误的代码因为一个非指针的临时变量 tmp, 导致垃圾收集器无法正确识别这个是一个指向变量 x 的指针. 第二个语句执行时, 变量 x 可能已经被转移, 临时变量 tmp 也就不在对应现在的 &x.b. 第三个赋值语句将彻底摧毁那个之前的那部分内存空间.

有很多类似原因导致的错误. 例如这条语句:

pT := uintptr(unsafe.Pointer(new(T))) // 提示: 错误!

这里并没有指针引用 new 新创建的变量, 因此语句执行完成之后, 垃圾收集器有权回收其内存空间, 所以返回的 pT 保存将是无效的地址.

目前的Go语言实现还没有使用移动GC(未来可能实现), 但这不该是侥幸的理由: 当前的Go实现已经有移动变量的场景. 在5.2节我们提到goroutine的栈是根据需要动态增长的. 当这个时候, 原来栈中的所以变量可能需要被移动到新的更大的栈中, 所以我们无法确保变量的地址在整个使用周期内保持不变.

在编写本文时, 还没有清晰的原则就指引Go程序员, 什么样 unsafe.Pointeruintptr 的转换是不安全的(参考 Go issue7192. 译注: 该问题已经修复.), 因此我们强烈建议按照最坏的方式处理. 将所有包含变量 y 地址的 uintptr 类型变量当作 BUG 处理, 同时减少不必要的 unsafe.Pointeruintptr 的转换. 在第一个例子中, 有三个到 uintptr 的转换, 字段偏移量的运算, 所有的转换全在一个表达式完成.

当调用一个库函数, 并且返回的是 uintptr 类型是, 比如下面反射包中的相关函数, 返回的结果应该立即转换为 unsafe.Pointer 以确保指针指向的是相同的变量.

package reflect

func (Value) Pointer() uintptr
func (Value) UnsafeAddr() uintptr
func (Value) InterfaceData() [2]uintptr // (index 1)