Go语言之unsafe

Golang Unsafe Pointer

Posted by alovn on May 5, 2021

Go语言类型系统是为了保证安全和效率设计的,但保证安全的同时也会使程序效率低下。unsafe包可以绕过Go语言的类型系统限制,直接对内存进行读写操作。例如我们通常不能操作一个未导出的变量,但是通过unsafe包就可以做到。

unsafe包提供了三个函数,它们都作用于编译期间:

1
2
3
func Sizeof(x ArbitraryType) uintptr //返回类型所占用的字节数(非指向内容的大小)。
func AlignOf(x ArbitraryType) uintptr //返回一个值在内存中的地址对齐值
func Offsetof(x ArbitraryType) uintptr //返回结构体的某个成员地址相对于此结构体起始处的字节数,即地址偏移。

指针

unsafe.Pointer 有两个重要的能力:

  1. unsafe.Pointer 可以和 任何类型的指针相互转换。
  2. unsafe.Pointer 可以和 uintptr 类型相互转换。

需要注意Go语言中指针有几个限制:

  1. Go 的指针不能进行数学运算。
  2. 不同类型的指针不能相互转换。
  3. 不同类型的指针不能使用 == 或 != 比较。

uintptr 是Golang的内置类型,是用于存储指针的整型。uintptr 可以进行数学运算。

unsafe.Pointer 可以转换为 uintptr。那么结合使用 uintptr 和 unsafe.Pointer 就可以解决 Go 指针不能进行数学运算的限制。

需要注意:使用uintptr操作指针数据是危险的,因为不能保证该地址的内存块没有被回收、或已被重新分配。

修改结构体成员值

对于一个结构体,通过 unsafe.Offset 函数可以通过获取结构体成员的偏移量,进而获取到结构体成员的地址,读写该地址的内存,也就可以修改成员值。

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
package main

import (
    "fmt"
    "unsafe"
)

type Person struct {
    name string
    age  int
}

func main() {
    p := Person{
        name: "hi",
        age:  18,
    }
    fmt.Println(unsafe.Sizeof(p)) //24
    fmt.Println(unsafe.Alignof(p)) //8
    fmt.Println(unsafe.Offsetof(p.name)) //0
    fmt.Println(unsafe.Offsetof(p.age)) //16

    // 修改未导出变量 name
    name := (*string)(unsafe.Pointer(&p))
    *name = "test"
    fmt.Printf("%+v\n", p) //{name:test age:18}
    
    // 修改未导出变量 age
    age := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.age)))
    *age = 11
    fmt.Printf("%+v\n", p) //{name:test age:11}
}

string 和 slice 转换

利用unsafe包可以做到字符串和bytes切片零拷贝的转换。这里需要先了解下slice和string的数据结构:

1
2
3
4
5
6
7
8
9
10
type StringHeader struct {
    Data uintptr
    Len int
}

type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
}

代码实现比较简单:

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
package main

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

func string2bytes(s string) []byte {
    stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
    sh := reflect.SliceHeader{
        Data: stringHeader.Data,
        Len:  stringHeader.Len,
        Cap:  stringHeader.Len,
    }
    return *(*[]byte)(unsafe.Pointer(&sh))
}

func bytes2string(b []byte) string {
    sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    sh := reflect.StringHeader{
        Data: sliceHeader.Data,
        Len:  sliceHeader.Len,
    }
    return *(*string)(unsafe.Pointer(&sh))
}
func main() {
    s := "abc"
    b := string2bytes(s)
    fmt.Println(b)
    
    s1 := bytes2string(b)
    fmt.Println(s1)
}

使用 unsafe 包可以直接操作内存而绕过 Go 的类型系统限制。Go 语言的源码中大量使用了 unsafe 包,某些场景下使用 unsafe 包中的函数会提升代码的执行效率,但是通过包的名称可以看出来它是不安全的,使用它会有一定的风险,如果对它不是太了解的话最好还是尽量避免使用。