NotePublic/Software/Development/Language/Go/Basic/Pointer/Golang_的指针.md

6.3 KiB
Raw Blame History

Golang 的指针

1. unsafe.Pointer

unsafe.Pointer 其实就是类似 C 的 void*,在 golang 中是用于各种指针相互转换的桥梁。uintptr 是 golang 的内置类型是能存储指针的整型uintptr 的底层类型是 int它和 unsafe.Pointer 可相互转换。uintptr 和 unsafe.Pointer 的区别就是unsafe.Pointer 只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;而 uintptr 是用于指针运算的GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象uintptr 类型的目标会被回收。golang 的 unsafe 包很强大,基本上很少会去用它。它可以像 C 一样去操作内存,但由于 golang 不支持直接进行指针运算,所以用起来稍显麻烦。

利用 unsafe.Pointer 来突破私有成员:

package p

import ("fmt")

type V struct {
    i int32
    j int64
}

func (this V) PutI() {
   fmt.Printf("i=%d\n", this.i)
}

func (this V) PutJ() {
    fmt.Printf("j=%d\n", this.j)
}

通过 unsafe 包来实现对 V 的成员 i 和 j 赋值,然后通过 PutI() 和 PutJ() 来打印观察输出结果:

package main

import (
    "poit/p"
    "unsafe"
)

func main() {
    var v *p.V = new(p.V)

    var i *int32 = (*int32)(unsafe.Pointer(v))
    *i = int32(98)

    var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int32(0)))))
    *j = int64(763)

    v.PutI()
    v.PutJ()
}

核心思想就是:结构体的成员在内存中的分配是一段连续的内存,结构体中第一个成员的地址就是这个结构体的地址,您也可以认为是相对于这个结构体偏移了 0。相同的这个结构体中的任一成员都可以相对于这个结构体的偏移来计算出它在内存中的绝对地址。

2. unsafe.Sizeof、Alignof 和 Offsetof

函数 unsafe.Sizeof 报告传递给它的参数在内存中占用的字节Byte长度1Byte=8bit1个字节是8位参数可以是任意类型的表达式但它不会对表达式进行求值。对 Sizeof 的调用会返回一个 uintptr 类型的常量表达式,所以返回的结果可以作为数组类型的长度大小,或者用作计算其他的常量:

fmt.Println(unsafe.Sizeof(float64(0))) // "8"
fmt.Println(unsafe.Sizeof(uint8(0))) // "1"

函数 Sizeof 仅报告每个数据结构固定部分的内存占用的字节长度。以字符串为例,报告的只是字符串对应的指针的字节长度,而不是字符串内容的长度:

func main() {
    var x string
    x = "a"
    fmt.Println(unsafe.Sizeof(x), len(x)) // "16 1"

    var s []string
    for i := 0; i < 10000; i++ {
        s = append(s, "Hello")
    }
    x = strings.Join(s, ", ")
    fmt.Println(unsafe.Sizeof(x), len(x)) // "16 69998"
}

无论字符串多长unsafe.Sizeof 返回的大小总是一样的。

函数 unsafe.Alignof 报告它参数类型所要求的对齐方式。和 Sizeof 一样,它的参数可以是任意类型的表达式,并且返回一个常量。通常情况下布尔和数值类型对齐到它们的长度(最多8个字节), 其它的类型则按字word对齐。

函数 unsafe.Offsetof参数必须是结构体 x 的一个字段 x.f。函数返回 f 相对于结构体 x 起始地址的偏移值,如果有内存空位,也会计算在内。

虽然这几个函数在不安全的unsafe包里但是这几个函数是安全的特别在需要优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助。

3. 不要把 uintptr 类型赋值给临时变量

下面这段代码看似和上面的一样的,引入了一个临时变量 tmp让把原来的一行拆成了两行这里的 tmp 是 uintptr 类型。这种引入 uintptr 类型的临时变量,破坏原来整行代码的用法是错误的:

func main() {
    tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
    pb := (*int64)(unsafe.Pointer(tmp))
    *pb = 42
    fmt.Println(x.b)
}

原因很微妙。一些垃圾回收器会把内存中变量移来移去以减少内存碎片等问题。这种类型的垃圾回收器称为移动GC。当一个变量在内存中移动后所有保存该变量旧地址的指针必须同时被更新为变量移动后的新地址。从垃圾回收器的角度看unsafe.Pointer 是一个变量指针,当变量移动后它的值也会被更新。而 uintptr 仅仅是一个数值,在垃圾回收的时候这个值是不会变的。

类似的错误用法还有像下面这样:

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

当垃圾回收器将会在语句执行结束后回收内存在这之后pT存储的是变量的旧地址而这个时候这个地址对应的已经不是那个变量了。

目前Go语言还没有使用移动GC所以上面的错误用法很多时候是可以正确运行的运行了几次都没有出错。但是还是存在其他移动变量的场景。

这样的代码能够通过编译并运行,编译器不会报错,不过会给一个提示性的错误信息:

possible misuse of unsafe.Pointer

所以还是可以在编译的时候发现的。这里强烈建议遵守最小可用原则,不要使用任何包含变量地址的 uintptr 类型的变量,并减少不必要的 unsafe.Pointer 类型到 uintptr 类型的转换。像本小节第一个例子里那样,转换为 uintptr 类型,最终在转换回 unsafe.Pointer 类型的操作,都要在一条语句中完成。

4. reflect 包返回的 uintptr

调用一个库函数,并且返回的是 uintptr 类型地址时,比如下面的 reflect 包中的几个函数。这些结果应该立刻转换为 unsafe.Pointer 来确保它们在接下来代码中能够始终指向原来的变量:

package reflect

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

一般的函数尽量不要返回 uintptr 类型,可能也就反射这类底层编程的包有这种情况。