unsafe包

标签:Go语言首次发布:2024-06-18最近修改:2024-09-02

Go 语言的unsafe包提供了一些用于进行低级内存操作的工具,允许程序员绕过 Go 语言的类型安全和内存安全机制。这些工具通常用于高级编程任务,例如与系统接口交互或进行性能优化。不过,使用unsafe包时需要非常小心,因为它可以导致程序的不稳定性和不可预知的行为。

API

类型

  1. type ArbitraryType int:ArbitraryType 用于表示任意类型的占位符类型。在实际使用中并不会直接使用这个类型,而是通过其他方式进行类型转换。
  2. type Pointer *ArbitraryType:Pointer 用于表示任意类型指针的通用指针,类似于 C 语言当中的*void。可以将任意类型的指针转换为 Pointer,反之亦然。
  3. type IntegerType int:IntegerType 在 unsafe 包当中的语义就是为了表示数据类型的长度。

函数

  1. func Sizeof(x ArbitraryType) uintptr:返回变量 x 的类型的大小,以字节为单位。可以用于获取任意类型的大小。

    go
    func main() {    var a bool    var b string    var c struct {        name string        age  int    }    fmt.Println(unsafe.Sizeof(a))      // 1    fmt.Println(unsafe.Sizeof(b))      // 16    fmt.Println(unsafe.Sizeof(c))      // 24    fmt.Println(unsafe.Sizeof(c.name)) // 16    fmt.Println(unsafe.Sizeof(c.age))  // 8}
  2. func Alignof(x ArbitraryType) uintptr:返回变量 x 的对齐值。对齐值是该类型变量在内存中对齐的字节数。

    go
    func main() {    var a []int    var b float32    var c byte    fmt.Println(unsafe.Alignof(a)) // 8    fmt.Println(unsafe.Alignof(b)) // 4    fmt.Println(unsafe.Alignof(c)) // 1    type myStruct struct {        x byte        y bool        z [3]float64    }    var s myStruct    fmt.Println(unsafe.Alignof(s))   // 8    fmt.Println(unsafe.Alignof(s.x)) // 1    fmt.Println(unsafe.Alignof(s.y)) // 1    fmt.Println(unsafe.Alignof(s.z)) // 8}
  3. func Offsetof(x ArbitraryType) uintptr:返回结构体字段 x 相对于其所在结构体起始位置的字节偏移量。

    go
    func main() {    type myStruct struct {        x int        y []bool        z float32    }    var m myStruct    fmt.Println(unsafe.Offsetof(m.x)) // 0    fmt.Println(unsafe.Offsetof(m.y)) // 8    fmt.Println(unsafe.Offsetof(m.z)) // 32}
  4. func Add(ptr Pointer, len IntegerType) Pointer 用于在指针 ptr 上加上一个偏移量 len,返回新的指针。这个函数通常用于指针算术操作。

  5. Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType 用于将一个指向数组或内存块的指针的前 len 个字节转换为切片。

  6. SliceData(slice []ArbitraryType) *ArbitraryType 用于访问切片的第一个元素的地址。

  7. String(ptr *byte, len IntegerType) string 用于将 ptr 指针指向的字节数组的前 len 个字节转换为字符串。

  8. StringData(str string) *byte 用于获取字符串底层字节数组的指针。

常见用法

string 和 [ ]byte 相互转换

字节数组转换为字符串

  1. 首先需要知道:uintptr 是 Go 语言的内置类型之一,其本质是一种无符号整数类型,它的大小与指针的大小相同。uintptr 常与 unsafe.Pointer 一起使用,以便进行指针运算。因为 unsafe.Pointer 不能直接参与运算,而 uintptr 是一个整数类型,可以进行加减运算等。
  2. 在 Go 语言中,[]byte 切片是一个结构体,包含一个指向底层字节数组的指针、切片的长度和容量。
  3. 所以首先需要获取切片的地址,并将其转换为unsafe.Pointer类型,作为一种通用的指针类型。
  4. 将 unsafe.Pointer 类型转换为*[3]uintptr 类型(一个数组指针,该指针指向一个有三个元素的数组,数组的类型为 uintptr),该数组里面的三个元素就是字节切片那个结构体里面的三个成员变量。
  5. 获取数组的第一个元素(字节切片指向底层字节数组的指针)和第二个元素(字节切片的长度),将这两个元素放在一个新的 uintptr 数组当中,为以后转换为字符串做准备。
  6. 将这个[2]uintptr 数组转换为 unsafe.Pointer 类型,然后再将 unsafe.Pointer 类型转换为字符串类型。
go
func BytesToString(b []byte) string {    // 1.获取字节切片的地址    bytePtr := unsafe.Pointer(&b)    // 2.获取字节切片底层字节数组的头部信息    sliceHeaderPtr := (*[3]uintptr)(bytePtr)    sliceHeader := *sliceHeaderPtr    // 3.创建一个新的字符串头    strHeader := [2]uintptr{sliceHeader[0], sliceHeader[1]}    // 4.转换为 unsafe.Pointer 类型    strHeaderPtr := unsafe.Pointer(&strHeader)    // 5.转换为 string 类型    str := (*string)(strHeaderPtr)    return *str}func main() {    b := []byte{'h', 'e', 'l', 'l', 'o'}    fmt.Println("***********方法1:通过unsafe.Pointer类型转换***********")    fmt.Printf("转换前的字节切片的底层字节数组地址: %x\n", uintptr(unsafe.Pointer(&b[0])))    s := BytesToString(b)    fmt.Printf("转换后的字符串的底层字节数组地址: %x\n", *(*uintptr)(unsafe.Pointer(&s)))    fmt.Println(s)    fmt.Println("**************方法2:string()强制类型转换**************")    fmt.Printf("转换前的字节切片的底层字节数组地址: %x\n", uintptr(unsafe.Pointer(&b[0])))    str := string(b)    fmt.Printf("转换后的字符串的底层字节数组地址: %x\n", *(*uintptr)(unsafe.Pointer(&str)))    fmt.Println(str)}

运行结果如下:

bash
***********方法1:通过unsafe.Pointer类型转换***********转换前的字节切片的底层字节数组地址: c0000a7e5b转换后的字符串的底层字节数组地址: c0000a7e5bhello**************方法2:string()强制类型转换**************转换前的字节切片的底层字节数组地址: c0000a7e5b转换后的字符串的底层字节数组地址: c0000960b0hello

可以看到使用 unsafe.Pointer 进行类型转换不会发生内存拷贝,底层的字节数组还是同一个对象;但是使用 string()强制类型转换就会发生数据拷贝。

以上的转换过程比较复杂,下面是更简单的转换过程:

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

利用的原理就是字节切片和字符串的底层数据结构相似的特点,也就是转换前后底层数据结构的内存布局是相对兼容的(转换后字节切片的容量字段会被忽略)。

字符串转换为字节数组

go
func StringToBytes(s string) []byte {    // 1.获取字符串的地址    strPtr := unsafe.Pointer(&s)    // 2.获取字符串头部    strHeader := *(*[2]uintptr)(strPtr)    // 3.构造一个字节切片的头部(容量和长度相同)    byteSlice := [3]uintptr{strHeader[0], strHeader[1], strHeader[1]}    // 4.将字节切片头部转换为字节切片    return *(*[]byte)(unsafe.Pointer(&byteSlice))}func main() {    str := "hello"    fmt.Println("***********方法1:通过unsafe.Pointer类型转换***********")    fmt.Printf("转换前的字符串的底层字节数组地址: %x\n", *(*uintptr)(unsafe.Pointer(&str)))    byteSlice := StringToBytes(str)    fmt.Printf("转换后的字节切片的底层字节数组地址: %x\n", *(*uintptr)(unsafe.Pointer(&byteSlice)))    fmt.Println(byteSlice)    fmt.Println("容量:", cap(byteSlice))    fmt.Println("***********方法2:通过[]byte强制类型转换***********")    fmt.Printf("转换前的字符串的底层字节数组地址: %x\n", *(*uintptr)(unsafe.Pointer(&str)))    byteSlice = []byte(str)    fmt.Printf("转换后的字节切片的底层字节数组地址: %x\n", *(*uintptr)(unsafe.Pointer(&byteSlice)))    fmt.Println(byteSlice)    fmt.Println("容量:", cap(byteSlice))}

运行结果如下:

bash
***********方法1:通过unsafe.Pointer类型转换***********转换前的字符串的底层字节数组地址: d581ba转换后的字节切片的底层字节数组地址: d581ba[104 101 108 108 111]容量: 5***********方法2:通过[]byte强制类型转换***********转换前的字符串的底层字节数组地址: d581ba转换后的字节切片的底层字节数组地址: c00000a100[104 101 108 108 111]容量: 8

当然也有简化后的转换版本:

go
func StringToBytes(s string) []byte {    return *(*[]byte)(unsafe.Pointer(&s))}

原理同样是利用了字节切片和字符串的底层数据结构相似的特点。但是由于字节切片比字符串多了一个容量字段,那么通过该方法转换后的容量是多少呢?运行结果如下:

bash
***********方法1:通过unsafe.Pointer类型转换***********转换前的字符串的底层字节数组地址: 1b81ba转换后的字节切片的底层字节数组地址: 1b81ba[104 101 108 108 111]容量 1209110***********方法2:通过[]byte强制类型转换***********转换前的字符串的底层字节数组地址: 1b81ba转换后的字节切片的底层字节数组地址: c0000960a8[104 101 108 108 111]容量 8

可以看到,由于在转换的过程当中没有考虑到容量的赋值,导致字节切片的容器变得非常大,有时候不排除会导致程序崩溃。

修改结构体私有成员变量的值

常规做法

  1. private 包:

    go
    package privatetype User struct {    Name string    age  int}func (u *User) SetAge(age int) {    u.age = age}
  2. main 包:

    go
    package mainimport (    "awesomeProject/unsafeStudy/private"    "fmt")func main() {    u := private.User{        Name: "abc",    }    u.SetAge(20)    fmt.Println(u) // {abc 20}}

使用 unsafe 包

go
func main() {    u := private.User{        Name: "xyz",    }    age := (*int)(unsafe.Add(unsafe.Pointer(&u), unsafe.Sizeof(u.Name)))    *age = 30    fmt.Println(u) // {xyz 30}}

其实就是计算出私有成员变量 age 的地址,修改该地址处的值即可。

进行指针运算

go
func main() {    // 定义一个数组,索引为3的值是3,索引为9的值是9,索引为11的值是11    array := [12]int{3: 3, 9: 9, 11: 11}    // 获取数组中元素的大小    elementSize := unsafe.Sizeof(array[0])    // 获取下表为9的元素的地址    p9 := &array[9]    // 获取下表为3的元素的值    p3 := unsafe.Pointer(uintptr(unsafe.Pointer(p9)) - elementSize*6)    fmt.Println(*(*int)(p3))    // 数组越界访问    px := unsafe.Add(unsafe.Pointer(p9), elementSize*5)    fmt.Println(*(*int)(px))}

运行结果如下:

bash
32891328

字符串不能强制修改

下面是强制性的修改字符串的代码示例,运行后直接发生 panic。

go
func ChangeStringValue(s string) string {    strPtr := (*byte)(unsafe.Pointer(&s))    byteSlice := unsafe.Slice(strPtr, len(s))    // 修改字节数组的内容    byteSlice[0] = 'w'    byteSlice[1] = 'o'    byteSlice[2] = 'r'    byteSlice[3] = 'l'    byteSlice[4] = 'd'    return s}func main() {    str := "hello"    changeStr := ChangeStringValue(str)    fmt.Println(changeStr)}

像 C++语言中的 string,其本身拥有内存空间,修改 string 是支持的。但 Go 的实现中,string 不包含内存空间,只有一个内存的指针,这样做的好处是 string 变得非常轻量,可以很方便的进行传递而不用担心内存拷贝。因为 string 通常指向字符串字面量,而字符串字面量存储位置是只读段,而不是堆或栈上,所以才有了 string 不可修改的约定。

Go 语言的设计者将字符串放在只读段(只读数据段)而不是栈上,主要有以下几个原因:

  1. 安全性:将字符串放在只读段可以防止程序无意或恶意地修改字符串内容,从而提高程序的安全性。由于只读段是不可修改的,任何试图修改只读段内容的操作都会导致程序崩溃。
  2. 内存管理的效率:
    • 共享内存: 如果多个字符串具有相同的内容,它们可以共享同一个内存位置。这在某些情况下可以显著减少内存的使用。
    • 减少复制: 当字符串在不同函数间传递时,只需要传递字符串的指针和长度,而不需要复制字符串的实际内容。
    • 简化垃圾回收:Go 语言使用垃圾回收机制来管理内存。将字符串放在只读段可以简化垃圾回收的实现,因为垃圾回收器不需要跟踪和回收这些不可变的字符串。

下面是相关 demo 来体现内存管理的效率:

  1. 共享内存

    go
    func main() {    // 创建两个具有相同内容的字符串    str1 := "hello"    str2 := "hello"    // 打印字符串的指针地址    fmt.Printf("字符串1的地址: %p\n", &str1)    fmt.Printf("字符串2的地址: %p\n", &str2)    // 获取字符串内容的内存地址    fmt.Printf("字符串1底层字节数组的地址: %x\n", *(*uintptr)(unsafe.Pointer(&str1)))    fmt.Printf("字符串2底层字节数组的地址: %x\n", *(*uintptr)(unsafe.Pointer(&str2)))}

    运行结果:

    bash
    字符串1的地址: 0xc000020070字符串2的地址: 0xc000020080字符串1底层字节数组的地址: f271ba字符串2底层字节数组的地址: f271ba
  2. 减少复制

    go
    func printStringAddress(s string) {    fmt.Printf("函数调用后的字符串地址: %p\n", &s)    fmt.Printf("函数调用后的字符串的底层字节数组的地址: %x\n", *(*uintptr)(unsafe.Pointer(&s)))}func main() {    str := "hello"    fmt.Printf("函数调用前的字符串地址: %p\n", &str)    fmt.Printf("函数调用前的字符串的底层字节数组的地址: %x\n", *(*uintptr)(unsafe.Pointer(&str)))    printStringAddress(str)}

    运行结果:

    bash
    函数调用前的字符串地址: 0xc00008a030函数调用前的字符串的底层字节数组的地址: 7471ba函数调用后的字符串地址: 0xc00008a040函数调用后的字符串的底层字节数组的地址: 7471ba