Go学习笔记–go指针:unsafe.Pointer&& uintptr类型

Go学习笔记–go指针:unsafe.Pointer&& uintptr类型
[TOC]

简单示例

Go语言是个强类型语言。也就是说Go对类型要求严格,不同类型不能进行赋值操作。指针也是具有明确类型的对象,进行严格类型检查。下面的代码会产生编译错误 :

package main
import (
    "fmt"
)
func main() {
    u := uint32(32)
    i := int32(1)
    fmt.Println(&u, &i) // 打印出地址
    p := &i // p 的类型是 *int32
    p = &u // &u的类型是 *uint32,于 p 的类型不同,不能赋值    
    p = (*int32)(&u) // 这种类型转换语法也是无效的  
    fmt.Println(p)
}

unsafe 包提供的Pointer方法可以完成这个任务

package main
import (
    "fmt"
    "unsafe"
)
func main() {
    u := uint32(32)
    i := int32(1)
    fmt.Println(&u, &i)
    p := &i
    p = (*int32)(unsafe.Pointer(&u))
    fmt.Println(p)
}

看起来unsafe.Pointer像是C语言中的void指针,没有任何类型信息。

官方说法

(1)任何类型的指针都可以被转化为Pointer
(2)Pointer可以被转化为任何类型的指针
(3)uintptr可以被转化为Pointer
(4)Pointer可以被转化为uintptr

大多数指针类型会写成T,表示是“一个指向T类型变量的指针”。unsafe.Pointer是特别定义的一种指针,它可以包含任意类型变量的地址。当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。

一个普通的T类型指针可以被转化为unsafe.Pointer类型指针,并且一个unsafe.Pointer类型指针也可以被转回普通的指针,被转回普通的指针类型并不需要和原始的T类型相同。

示例:通过将float64类型指针转化为uint64类型指针,我们可以查看一个浮点数变量的位模式。

package main

import (
    "fmt"
    "unsafe"
    "reflect"
)

func Float64bits(f float64) uint64 {
    fmt.Println(reflect.TypeOf(unsafe.Pointer(&f)))  //unsafe.Pointer
    fmt.Println(reflect.TypeOf((*uint64)(unsafe.Pointer(&f))))  //*uint64
    return *(*uint64)(unsafe.Pointer(&f))
}

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

可见unsafe.Pointer的作用就是 在不同类型中串联。A类型->Pointer->B类型。所以这里就需要写代码的人自己注意了。因为有时候类型不同会导致很严重的问题。譬如整形转到struct类型肯定会出事。

uintptr

// uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
type uintptr uintptr

uintptr是golang的内置类型,是能存储指针的整型,在64位平台上底层的数据类型定义如下:

typedef unsigned long long int  uint64;
typedef uint64          uintptr;

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

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

package main

import (
    "fmt"
    "unsafe"
)

func main() {

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

    /**
    unsafe.Offsetof 函数的参数必须是一个字段 x.f, 然后返回 f 字段相对于 x 起始地址的偏移量, 包括可能的空洞.
    */

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

上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性(注:这是真正可以体会unsafe包为何不安全的例子)。

下面段代码是错误的:

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地址。
第三个向之前无效地址空间的赋值语句将彻底摧毁整个程序!

总结

unsafe.Pointer 就是类似C语言的void指针。
而uintptr 就是用于存放指针值的类型。