gopl-zh.github.com/ch13/ch13-02.md
fuyc 8fda418f3a fix typo and optimize.
Change-Id: I7b6938936231fd722814984678ffa30402539fd9
2016-08-11 17:08:38 +08:00

5.5 KiB
Raw Blame History

13.2. unsafe.Pointer

大多数指针类型会写成*T表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指针类型译注类似C语言中的void*类型的指针),它可以包含任意类型变量的地址。当然,我们不可以直接通过*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类型的临时变量因为它可能会破坏代码的安全性译注这是真正可以体会unsafe包为何不安全的例子。下面段代码是错误的

// NOTE: subtly incorrect!
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42

产生错误的原因很微妙。有时候垃圾回收器会移动一些变量以降低内存碎片等问题。这类垃圾回收器被称为移动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.Pointer和uintptr的转换是不安全的参考 Issue7192 . 译注: 该问题已经关闭因此我们强烈建议按照最坏的方式处理。将所有包含变量地址的uintptr类型变量当作BUG处理同时减少不必要的unsafe.Pointer类型到uintptr类型的转换。在第一个例子中有三个转换——字段偏移量到uintptr的转换和转回unsafe.Pointer类型的操作——所有的转换全在一个表达式完成。

当调用一个库函数并且返回的是uintptr类型地址时译注普通方法实现的函数尽量不要返回该类型。下面例子是reflect包的函数reflect包和unsafe包一样都是采用特殊技术实现的编译器可能给它们开了后门比如下面反射包中的相关函数返回的结果应该立即转换为unsafe.Pointer以确保指针指向的是相同的变量。

package reflect

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