命令记不住

登陆

标准库 unsafe 包

标准库unsafe包

1.unsafe包简介

在unsafe包中,只提供了三个函数,两个类型

  • func Alignof(x ArbitraryType) uintptr
  • func Offsetof(x ArbitraryType) uintptr
  • func Sizeof(x ArbitraryType) uintptr
  • type Arbitrary Type int
  • type Pointer *ArbitraryType

通过unsafe包,可以完成类型指针运算的操作

ArbitraryType是以int为基础定义的一个新类型,但在unsafe包中,对ArbitraryType赋予了特殊的意义,通常把interface{}看作任意类型,而ArbitraryType这个类型,比interface{}还要随意

Pointer是以ArbitraryType指针类型为基础的新类型,在GO语言系统中 ,可以把Pointer类型理解成任何指针的“亲爹“

Go语言的指针与int类型在内存中占用的字节数是一样的。ArbitraryType类型的变量也可以是指针

通过分析发现,unsafe包中三个函数的参数均是ArbitraryType类型。

  • Alignof返回变量对齐字节数量
  • Offsetof返回变量指定属性的偏移量,所以如果变量是一个struct类型,不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的属性当作参数
  • Sizeof返回变量在内存中占用的字节数,切记,如果是slice,则不会返回这个slice在内存中的实际占用长度

在unsafe包中,通过ArbitraryType、Pointer这两个类型,可以将其他类型都转换过来,然后通过这三个函数,分别取长度、偏移量、对齐字节数,就可以在内存地址映射中来回游走

2.指针运算

GO语言在语法上不支持指针类型直接进行指针运算,例如要在某个指针地址上加上偏移量,指针类型 是不能做这个运算的,但是!可以靠uintptr了类型,可以将指针类型先转成uintptr类型,做完地址加减法运算后,再转换成指针类型,通过*操作达到取值、修改值的目的,在GO语言中,uintptr这个基础类型的字节长度与int一致

unsafe.Pointer在GO语言中是用于各种指针相互转换的桥梁,既通用指针,它可以任意类型的指针实现相互转换,也可以将任意类型的指针转换为uintptr进行指针运算

uintptr是GO语言的内置类型,是能存储指针的整型,unitptr的底层类型是int,它和unsafe.Pointer可相互转换

uintptr和unsafe.Pointer的区别是:

  • unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型的指针,不可以参与指针运算
  • uintptr是用于指针运算的,GC不把uintptr当指针,既uintptr无法持有对象,uintptr类型的目标会被回收
  • unsafe.Pointer可以和普通指针进行相互转换
  • unsafe.Pointer可以和uintptr进行相互转换

Go语言的unsafe包很强大,可以操作内存。

uintptr和intptr是无符号和有符号的指针类型,并且确保在64位平台上的长度是8B,在32位的平台是4B,uintptr主要用于GO语言中的指针运算。

以下main.go源代码通过unsafe包来实现对结构体V的成员i和j赋值,然后通过GetI()和GetJ来打印观察输出结果

package main

import (
    "fmt"
    "unsafe"
)

type V struct {
    i int32
    j int64
}

func (v V) GetI() {
    fmt.Printf("i=%d\n", v.i)
}
func (v V) GetJ() {
    fmt.Printf("j=%d\n", v.j)
}

func main() {
    // 定义指针类型变量
    var v *V = &V{199, 299}

    // 取得v的指针并转为*int32的值,对应结构体的i。
    var i *int32 = (*int32)(unsafe.Pointer(v))

    fmt.Println("指针地址:", i)
    fmt.Println("指针uintptr值:", uintptr(unsafe.Pointer(i)))
    *i = int32(98)

    // 根据v的基准地址加上偏移量进行指针运算,运算后的值为j的地址,使用unsafe.Pointer转为指针
    var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))

    *j = int64(763)

    v.GetI()
    v.GetJ()
}

程序输出

指针地址: 0xc000126010
指针uintptr值: 824634925072
i=98
j=763

要修改struct字段的值,需要提前知道结构体V的成员布局,然后根据字段计算偏移量,以及考虑对齐值,最后通过指针运算得到成员指针,利用指针达到修改成员值的目的。由于结构体的成员在内存中的分配是一段连续的内存,因此结构体中第一个成员的地址就是这个结构体的地址,也可以认为是相对于这个结构体偏移了0.同样,这个结构体中的任一成员都可以相对于这个结构体的偏移量计算出自己在内存中的绝对地址

上面的main方法的现实如下:

var v *V = &V{199,299}

通过&来分配一段内存(并按类型初始化),返回一个指针。所以v就是类型 为V的一个指针,和new函数的作用类似

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

将指针转成通用指针,再转成int32指针类型。这里就看到unsafe.Pointer的作用了,不能直接将v转成int32类型的指针,那样会发生运行时异常,但是unsafe.Point可以转为任何指针,刚才说了v的地址其实就是它的第一个成员的地址,所以这个i就很显然指向了v的成员i,通过给I赋值就相当于给v.i赋值

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

v 有两个成员i和j,并且在定义中,i位于j的前面,而i是int32类型,也就是说i占4B.所以j相对于v偏移了4B,可以用uintptr(4)或uintptr(unsafe.Sizeof(int64(0)))来进行偏移。unsafe.Sizeof方法用来得到一个值应该占用多个字节空间。

之所以转成unitptr类型,是因为需要做指针运算。v的地址加上j相对于v的偏移地址,就得到了v.j在内存中的绝对地址,然后通过unsafe.Pointer转为指针。别忘了j的类型 是int64,所以现在的j就是一个指向v.j的指针,接下来给他赋值:

*j = int64(763)

另外,可以看到两种地址表示方式的差异如下:

  • 指针地址: 0xc000126010
  • 指针uintptr值: 824634925072

最后,通过两个Get函数得到结构体i,j的值,发现他们已经被修改。这两个成员是不能导出的,所以上面的方法也是修改这类不能导出成员的魔法功能

上面结构体V中,定义了两个成员属性,如果需要定义一个byte类型的成员属性,输出如下

package main

import (
    "fmt"
    "unsafe"
)

type V struct {
    b byte
    i int32
    j int64
}

func (v V) GetI() {
    fmt.Printf("i=%d\n", v.i)
}
func (v V) GetJ() {
    fmt.Printf("j=%d\n", v.j)
}

func main() {
    // 定义指针类型变量
    var v *V = new(V)

    // v的长度
    fmt.Printf("size=%d\n", unsafe.Sizeof(*v))
    // 取得v的指针考虑对齐值计算偏移量,然后转为*int32的值,对应结构体的i。
    var i *int32 = (*int32)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(4*unsafe.Sizeof(byte(0)))))

    fmt.Println("指针地址:", i)
    fmt.Println("指针uintptr值:", uintptr(unsafe.Pointer(i)))
    *i = int32(98)

    // 根据v的基准地址加上偏移量进行指针运算,运算后的值为j的地址,使用unsafe.Pointer转为指针
    var j *int64 = (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))

    *j = int64(763)
    fmt.Println("指针uintptr值:", uintptr(unsafe.Pointer(&v.b)))
    fmt.Println("指针uintptr值:", uintptr(unsafe.Pointer(&v.i)))
    fmt.Println("指针uintptr值:", uintptr(unsafe.Pointer(&v.j)))
    v.GetI()
    v.GetJ()
}

程序输出:

size=16
指针地址: 0xc00001c0e4
指针uintptr值: 824633835748
指针uintptr值: 824633835744
指针uintptr值: 824633835748
指针uintptr值: 824633835752
i=98
j=763

新结构体的长度为size=16,好像跟想象的不一致,下面计算一下:

b是byte类型,长度为1B,i是Int32类型,长度为4B; j是int64类型,长度为8B, 1+4+8=13.

这是因为发生了对齐,在struct中,它的对齐值是它的成员中的最大对齐值

每个成员类型都有它的对齐值,可以用unsafe.Alignof方法来计算,例如unsafe.Alignof(v.b)可以得到b的对齐值为1.但这个对齐值是其值类型的长度或引用的地址长度(32位或者64位),和其在结构体中的长度不是简单相加的关系。经过在64位计算机上测试,发现地址(uintptr)如下

指针uintptr值: 824633835744
指针uintptr值: 824633835748
指针uintptr值: 824633835752

可以初步判断,也经过测试验证,取i值使用uintptr(4 * unsafe.Sizeof(byte(0)))是准确的。至于size其实也和对齐值有关,也不是简单相加每个字段的长度

unsafe.Offsetof可以在实际中使用,如果改变私有的字段,需要按照上面的方法仔细确认好对齐值再进行操作

推荐阅读