一、背景

朋友发了一段测试代码里面不正确的使用了atomic.StorePointer,导致GC的时候程序Panic了。

var current int64
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&current)), unsafe.Pointer(&latest))

为什么会Panic这里先按下不表。之前对 unsafe.Pointer 用的并不多,也没有系统了解过。所以就想系统看下。看了下 unsafe.Pointer 官方文档还挺详细的,可能只之前使用出错的人太多了,所以 rsc 单独提了一个 CR 来说明unsafe.Pointer的用法。

二、unsafe.Pointer

unsafe.Pointer表示指向任意类型的指针,主要可以做下面4个操作:

  1. 任意类型的指针值都可以转换为unsafe.Pointer
  2. unsafe.Pointer可以转换为任意类型的指针值。
  3. uintptr可以转换为unsafe.Pointer
  4. unsafe.Pointer可以转换为uintptr

2.1 场景一 类型转换

unsafe.Pointer支持*T1*T2类型的转换,前提是T2类型要小于T1类型大小,比如reflect.SliceHeader转为reflect.StringHeader

func SliceByteToString(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

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

func Float64frombits(b uint64) float64 {
    return *(*float64)(unsafe.Pointer(&b))
}

2.2 场景二 unsafe.Pointer 转换为 uintptr

将指针转换为uintptr 生成指向值的内存地址,作为整数。 这种uintptr的通常用途是打印它。

  • uintptr转换回Pointer通常是无效的。(编译器会有Possible misuse of 'unsafe.Pointer' 警告
  • uintptr是一个整数,而不是一个引用。
  • 即使 uintptr保存了某个对象的地址,垃圾收集器也不会更新该uintptr的值。
func main() {
    type User struct{ age int }
    var t User
    fmt.Printf("%p\n", &t)           // 0xc000018270
    println(&t)                      // 0xc000018270
    p := uintptr(unsafe.Pointer(&t)) // c000018270
    fmt.Printf("Ox%x\n", p)          // 0xc000018270
}

2.3 场景三 计算 uintptr 得到 Pointer

如果p指向一个已分配的对象,则可以通过转换为uintptr、添加偏移量和转换回Pointer来推进该对象。此模式最常见的用途是访问结构中的字段或数组的元素:

func main() {
    type Num struct {
        i string
        j int64
    }
    n := Num{i: "test", j: 1}
    nPointer := unsafe.Pointer(&n)
    niPointer := (*string)(nPointer)
    *niPointer = "dr"
    njPointer := (*int64)(unsafe.Pointer(uintptr(nPointer) + unsafe.Offsetof(n.j)))
    *njPointer = 2
    fmt.Println(n) // {dr 2}
    
      // equivalent to e := unsafe.Pointer(&x[i])
    // e := unsafe.Pointer(uintptr(unsafe.Pointer(&x[0])) + i*unsafe.Sizeof(x[0]))
}

注意一:不要读到 Struct/String/[]Byte 尾部数据

// INVALID: end points outside allocated space.
var s thing
end = unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + unsafe.Sizeof(s))

// INVALID: end points outside allocated space.
b := make([]byte, n)
end = unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(n))

注意二:不要存储 uintptr 到变量【重要】

// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := uintptr(p)
p = unsafe.Pointer(u + offset)

func main() {
    type User struct{ age int }
    var t User
    fmt.Printf("%p\n", &t)
    p := uintptr(unsafe.Pointer(&t))
    fmt.Println((*User)(unsafe.Pointer(p))) // 执行 go vet, 这一行会有警告:possible misuse of unsafe.Pointer
}

为什么不支持用变量存储 uintptr ,然后转unsafe.Pointer

1. 一个值的生命范围可能并没有代码中看上去的大

type T struct {x int; y *[1<<23]byte}
func bar() {
    t := T{y: new([1<<23]byte)}
    p := uintptr(unsafe.Pointer(&t.y[0]))
    
    // 一个聪明的编译器能够觉察到值t.y将不会再被用到而回收之。
    *(*byte)(unsafe.Pointer(p)) = 1 // 危险操作!
    println(t.x) // ok。继续使用值t,但只使用t.x字段。
}

2. 栈扩容的时候地址会发生变化

func f(i int) int {
    if i == 0 || i == 1 {
        return i
    }
    return f(i - 1)
}

func main() {
    var num uint64
    xAddr := uintptr(unsafe.Pointer(&num)) 
    println("before stack copy num : ", num, " num pointer: ", &num)

    f(10000000)

    xPointer := (*uint64)(unsafe.Pointer(xAddr)) // 这里有警告 possible misuse of unsafe.Pointer
    atomic.AddUint64(xPointer, 1)
    println("after stack copy num : ", num, " num pointer:", &num)
}

 // 输出内容如下:    
before stack copy num :  0  num pointer:  0xc000044768
after stack copy num :  0  num pointer: 0xc0200fff68

注意三:请注意,Pointer必须指向已分配的对象,因此它可能不是 nil。

// INVALID: conversion of nil pointer
u := unsafe.Pointer(nil)
p := unsafe.Pointer(uintptr(u) + 1) 
fmt.Println(p1) // 0x1

注意四:*unsafe.Pointer是一个类型安全指针类型

func main() {
    type T struct {x int}
    var p *T
    var unsafePPT = (*unsafe.Pointer)(unsafe.Pointer(&p))
    atomic.StorePointer(unsafePPT, unsafe.Pointer(&T{123}))
    fmt.Println(p) // &{123}
}

2.4 场景四 在调用 syscall.Syscall 时将指针转换为 uintptr

syscall 包中的 Syscall 函数将它们的 uintptr 参数直接传递给操作系统,然后操作系统可能会根据调用的细节将其中一些重新解释为指针。也就是说,系统调用实现隐式地将某些参数转换回从 uintptr 到指针。

如果必须将指针参数转换为 uintptr 以用作参数,则该转换必须出现在调用表达式本身中:

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

编译器在调用汇编中实现的函数的参数列表中处理转换为 uintptr 的指针,方法是安排引用的分配对象(如果有)在调用完成之前保留并且不移动,即使从类型仅在通话期间似乎不再需要该对象。具体见 CR

为了让编译器识别这种模式,转换必须出现在参数列表中,下面这种方式是无效的:

// INVALID: uintptr cannot be stored in variable
// before implicit conversion back to Pointer during system call.
u := uintptr(unsafe.Pointer(p))
syscall.Syscall(SYS_READ, uintptr(fd), u, uintptr(n))

2.5 场景五 reflect.Value.Pointer 或 reflect.Value.UnsafeAddr 的结果从 uintptr 到 Pointer 的转换。

reflect.Value.Pointerreflect.Value.UnsafeAddr 返回的是uintptr也不能用变量存储。同场景三的注意事项二。

p := (*int)(unsafe.Pointer(reflect.ValueOf(new(int)).Pointer())) // ok

// INVALID: uintptr cannot be stored in variable
// before conversion back to Pointer.
u := reflect.ValueOf(new(int)).Pointer() // uintptr
p := (*int)(unsafe.Pointer(u))

2.6 场景六 reflect.SliceHeader 和 reflect.StringHeader 转换

场景一说了,大sizestruct转小sizestruct没有任何问题。比如:

func SliceByteToString(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

reflect.StringHeaderreflect.SliceHeader很多场景就会有问题,注意不要凭空生成SliceHeader和StringHeader,要从切片和字符串转换出它们。 详见 Runtime 代码注释

As in the previous case, the reflect data structures SliceHeader and StringHeader declare the field Data as a uintptr to keep callers from changing the result to an arbitrary type without first importing “unsafe”. However, this means that SliceHeader and StringHeader are only valid when interpreting the content of an actual slice or string value.

func main() {
    fmt.Printf("main : %s\n", gcStr())
}

func gcStr() []byte {
    defer runtime.GC()
    x := []byte("1234567890")
    return StringToSliceByte(string(x))
}

// 这个方法是凭空生成的一个reflect.SliceHeader,所以 s 被 gc 回收了, main 输出乱码
func StringToSliceByte(s string) []byte {
    l := len(s)
    return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
        Data: (*(*reflect.StringHeader)(unsafe.Pointer(&s))).Data,
        Len:  l,
        Cap:  l,
    }))
}

func StringToSliceByte2(s string) []byte {
    var b []byte // 这里申明了一个 slice 所以没有问题
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    sliceHeader.Data = stringHeader.Data
    sliceHeader.Len = stringHeader.Len
    sliceHeader.Cap = stringHeader.Len
    return b
}

StringToSliceByte2 是好的,是因为编译器对 reflect.StringHeader 做了优化 如果使用自定义的StringHeaderSliceHeader 依然有问题。

type StringHeader struct {
    Data uintptr // unsafe.Pointer
    Len  int
}

type SliceHeader struct {
    Data uintptr // unsafe.Pointer
    Len  int
    Cap  int
}

// https://groups.google.com/g/golang-nuts/c/Zsfk-VMd_fU/m/qJzdycRiCwAJ?pli=1
func StringToSliceByte3(s string) []byte {
    var b []byte // 这里申明了一个 slice 所以没有问题
    stringHeader := (*StringHeader)(unsafe.Pointer(&s))
    sliceHeader := (*SliceHeader)(unsafe.Pointer(&b))
    sliceHeader.Data = stringHeader.Data
    sliceHeader.Len = stringHeader.Len
    sliceHeader.Cap = stringHeader.Len
    return b
}

如果StringHeaderSliceHeaderData改成unsafe.Pointer,那StringToSliceByte3也能正常work。所以 mdempsky 就提议过,在unsafe包里面新增SliceString类型,方便做stringslice的转换。

type Slice struct {
    Data Pointer
    Len int
    Cap int
}

type String struct {
    Data Pointer
    Len int
}

func makestring(p *byte, n int) string {
    // Direct conversion of unsafe.String to string.
    return string(unsafe.String{unsafe.Pointer(p), n})
}

func memslice(p *byte, n int) (res []byte) {
    // Direct conversion of *[]byte to *unsafe.Slice, without using unsafe.Pointer.
    s := (*unsafe.Slice)(&res)
    s.Data = unsafe.Pointer(p)
    s.Len = n
    s.Cap = n
    return
}

推荐使用gin转换方式,简介明了:

func StringToBytes(s string) []byte {
    return *(*[]byte)(unsafe.Pointer(
        &struct {
            string
            Cap int
        }{s, len(s)},
    ))
}

三、总结

unsafe.Pointeruintptr 坑挺多的,使用的时候一定要注意。