golang-标准库(unsafe)

除了c家族的编程语言明确提供了直接通过指针运算操作内存的方式,其它语言大多屏蔽了这一操作,程序员直接操作内存往往不太安全,容易出现各种问题。go有着c的影子,保留了指针但又对指针的能力进行了削弱。在go中也不能直接进行指针运算来操作内存。但go对指针还是留了一扇不起眼的窗来进行指针运算,这扇窗就是unsafe包。

首先unsafe包的内容并不多,只有寥寥几个数据结构和函数。要使用unsafe包需要先了解内存对齐,对有c使用经验的朋友就比较简单了。数据在内存的存储并不是连续按着存放的,而是按照一定的对齐规则。使用对齐规则有一个最大的好处是避免cpu的二次读取,也就是说对齐后的数据cpu只需要读取一次。(此处不再细述原因,作者目前的知识层面也就到这儿,原因就是计算机的底层知识了)

unsafe,顾名思义,是不安全的,Go定义这个包名也是这个意思,让我们尽可能的不要使用它,如果你使用它,看到了这个名字,也会想到尽可能的不要使用它,或者更小心的使用它。

虽然这个包不安全,但是它也有它的优势,那就是可以绕过Go的内存安全机制,直接对内存进行读写,所以有时候因为性能的需要,会冒一些风险使用该包,对内存进行操作。在go的标准库中也有使用unsafe包的操作(比如:sync,runtime包等)

type Pointer unsafe包提供的一个类型,任何指针类型都可以转换为Pointer类型, Pointer类型也可以转换为任何指针类型。go中的指针不能进行运算(比如:*int),只能对数据进行读取和赋值。
uintptr 是go的一个数据类型,它不是unsafe包提供的,它是一个足够大的整数类型,可以容乃任何的指针。上面的 Pointer类型可以转换为 uintptr类型,反过来uintptr类型也可以转换为Pointer类型。uintptr类型才能进行指针运算。
  • 下面简单看一个用unsafe对int8类型变量的值进行修改的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package main

    import (
    "fmt"
    "unsafe"
    )

    func main() {
    var a int8 = 1 // 声明一个变量
    fmt.Println(a)
    // 先将a的指针转为Pointer类型,再将Pointer类型转为*int8指针
    i := (*int8)(unsafe.Pointer(&a))

    *i = 100 // 对转换来的新指针变量进行赋值操作,改变a的值
    fmt.Println(a)
    }
上面的操作看起来有点多此一举,但这里只是展示除了常规的对变量进行赋值操作外,还可以通过unsafe包对任何变量进行赋值操作。(把上面的a换成其它类型也可以,比如切片,map,结构体,只是转换的时候也要将 i 转换为对应类型的指针)。
  • 接下来看unsafe包的另外三个函数:
func Sizeof(v ArbitraryType) uintptr 计算变量在go中所占用的内存大小,返回类型v的对齐方式(即类型v在内存中占用的字节数);若是结构体类型的字段的形式,它会返回字段f在该结构体中的对齐方式
func Alignof(v ArbitraryType) uintptr 这个函数只有在参数v是结构体成员的时候才有用。返回类型v所代表的结构体字段在结构体中的偏移量,它必须为结构体类型的字段的形式。换句话说,它返回该结构起始处与该字段起始处之间的字节数。
func Offsetof(v ArbitraryType) uintptr
  • 下面看这三个函数的应用,还是通过一个例子:先看一下包结构,主目录下一个p包,包下面含有一个v.go文件。还有一个main文件

  • v.go文件内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package p

    import (
    "fmt"
    )

    // 声明一个结构体V,其中的三个属性都是私有的对外部包不可访问
    type V struct {
    i int32
    j int64
    k byte
    }
    // 定义一个打印方法,打印成员变量
    func (v *V) PrintInfo() {
    fmt.Printf("i=%d, j=%d, k=%d \n",v.i,v.j, v.k)
    }
    // 定义一个打印变量地址的方法
    func (v *V) PrintAddress() {
    fmt.Printf("i=%p, j=%p, k=%p \n",&v.i,&v.j, &v.k)
    }
  • main.go文件内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    package main

    import (
    "fmt"
    "point/p"
    "unsafe"
    )

    func main() {
    v := new(p.V) // 通过new声明一个p.V的类型指针
    fmt.Println(unsafe.Sizeof(*v)) // v 对应的结构体在go中占用的内存大小,为24字节
    /**
    上面为什么会是24字节呢,int32大小为4,int64大小为8,byte大小为1,加起来是13,这就是发生了内存对齐的原因。
    下面简单详述一下go中的内存对齐:
    1.对于具体类型来说,对齐值=min(编译器默认对齐值,类型大小Sizeof长度)。也就是在默认设置的对齐值和类型的内存占用大小之间,取最小值为该类型的对齐值。简单说就是这个类型的偏移量为它的对齐值的倍数关系。
    2.struct在每个字段都内存对齐之后,其本身也要进行对齐,对齐值=min(默认对齐值,字段最大类型长度)。这条也很好理解,struct的所有字段中,最大的那个类型的长度以及默认对齐值之间,取最小的那个。简单说就是结构体整体的大小为它最大类型长度的倍数。
    我们再来解释为什么*v占用的大小为24字节,第一个i占用4字节,它是结构体第一个,不需要对齐。
    第二个j占用8字节,它的对齐值通过 unsafe.Alignof(int64(0)) 函数得出也为8个字节,应用上面的第一条规则,前面只有四个字节的偏移量,4不是8的倍数,
    所以在i的后面需要填充4个字节:iiii----|jjjjjjjj。-为填充,一般是填充0。到这里整体就已经用了16个字节了。
    第三个k占用1个字节,它的对齐值通过 unsafe.Alignof(byte(0)) 函数得出也为1个字节。应用上面的第一条规则,16是1的倍数。不需要填充了。
    到这里v的字节占用变成了 iiii----|jjjjjjjj|k。接下里应用上面的第二条规则,17不是最大类型int64长度8的倍数,所以还需要填充。
    iiii----|jjjjjjjj|k------- ,在后面再填充7个字节,达到24字节后,就满足上面的第二条规则了。至此就是v为何占用24字节。
    */

    // 操作第一个私有变量 i
    // 因为在go中结构体的指针就对应是第一个成员变量的指针,所以可以将 v 转换为第一个成员变量 i 的指针
    i := (*int32)(unsafe.Pointer(v))
    *i = 100 // 对私有变量 i 进行赋值操作
    v.PrintInfo() // i=:100, j=0, k=0

    // 操作第二个私有变量 j
    // j到i偏移了8个字节
    j := (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0)))))
    *j = 200
    v.PrintInfo() // i=:100, j=200, k=0

    // 操作第三个私有变量 k
    // k到j偏移了8个字节
    k := (*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + uintptr(unsafe.Sizeof(int64(0))) + uintptr(unsafe.Sizeof(int64(0)))))
    *k = 97
    v.PrintInfo() // i=:100, j=200, k=97
    v.PrintAddress() // i=0xc000016180, j=0xc000016188, k=0xc000016190。可以看到三个变量的地址偏移量完全符合上面的内存对齐规则。(注意内存地址是16进制的)
    }

看完了上面的例子,基本上unsafe包的操作就算解释完了。可以看到unsafe包可以跳过go的类型检查,操作私有变量。甚至操作任意的内存地址。
  • 下面介绍unsafe包的一个特别用处:unsafe包可以转换成员属性类型相同,但名字不同的结构体,这是go的类型转换不能做到的。同样看例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package main

    import (
    "fmt"
    "unsafe"
    )
    // 声明两个结构体,它们的成员数据类型都相同,成员名不同。
    type T struct{
    a int8
    b string
    }
    type K struct {
    c int8
    d string
    }

    func main() {
    t := &T{100, "hello"}
    k := (*K)(unsafe.Pointer(t))
    fmt.Println(k) // &{100 hello} , 成功的把T类型变量转换为了K类型
    t.a = 99
    fmt.Println(k) // &{100 hello}, 改变t,对应k的值也变了。
    }
此外如果T结构体只有一个a,那么转化为K类型的时候,不会报错,K的d成员会忽略,采用默认值。同理,如果K只有c,没有d,也不会报错,转换的时候T类型的b会被忽略,只做a变量的转换。这里就不在展示了。
综上,unsafe包提供了绕过go类型检查的机制,直接让我们操作内存。unsafe包要慎用,目前作者也没找到unsafe包在实际编码过程中比较实用的地方。上面的类型转换说实话,实际编码中,用得也不多。有其它用处的地方,还望大家多多执教。
作者

itpika

发布于

2020-02-05 12:25:36

更新于

2021-01-28 16:44:10

许可协议

评论