Go语言unsafe包的用法(非常详细)
Go 语言内置的 unsafe 包能打破类型和内存安全机制,直接对内存进行读、写操作。
我们知道,Go 语言中类型之间的转换是显式而非隐式的,并且互相转换的类型要彼此兼容,因为不同的类型之间不能进行赋值操作。示例代码如下:
而 unsafe.Pointer 是打破这个限制的特殊类型!示例代码如下:
顾名思义,unsafe 是不安全的意思。Go 语言定义这个包名,就是告诉我们尽量不要使用它,如果要使用它,也要万般小心。
虽然这个包不安全,但它也有优点,即能打破 Go 语言的类型和内存安全机制,直接对内存进行读、写操作。有时,因为某些需要,我们会冒险使用该包对内存进行操作。
例如,当处理系统调用时,要求 Go 的结构体和 C 的结构体拥有相同的内存结构,那么此时使用 unsafe 包就是唯一的选项。
在 Go 语言中,指针类型不支持向指针地址添加偏移量的操作。那谁能这么做呢?答案是 uintptr 类型!
uintptr 是 Go 语言的内置类型,是存储指针的整型。uintptr 类型的底层是 int 类型(其字节长度与 int 类型一致),它与 unsafe.Pointer 类型可以相互转换。我们可以先将指针类型转换为 uintptr 类型,待做完地址加减运算后,再将其转换成指针类型,最后通过使用“*”达到取值和修改值的目的。
unsafe.Pointer 类型类似于 C语言中的 void *,它是 Go 语言中各种类型的指针相互转换的桥梁,它既可以让任意类型的指针相互转换,也可以将任意类型的指针转换为 uintptr 类型并进行指针运算。
uintptr 和 unsafe.Pointer 类型的区别如下表所示:
Sizeof() 函数用于返回类型所占用的内存大小,这个值仅与类型有关,与类型对应的变量存储的内容大小无关。比如布尔型占用 1 字节,int8 也占用 1 字节。对于不带长度的 int 类型,Sizeof() 函数执行的结果与平台有关。
示例代码如下:
示例代码如下:
示例代码如下:
提示,反射包中的函数 reflect.TypeOf(x).Align 也可以获取对齐值,它与 unsafe.Alignof(x) 等价。
在 Go语言中,字符串底层的结构体 stringStruct 由一个指向字节数组的 unsafe.Pointer 类型和表示长度的 int 类型组成,而结构体 StringHeader 则是字符串在运行时的表现形式。
需要注意的是,StringHeader 的 Data 字段不足以保证数据引用不会被当作垃圾回收,因此程序必须保留一个单独且指向基础数据的指针。
两个结构体的定义如下:
我们可以使用 unsafe 包中的 unsafe.Pointer 类型把字符串类型的指针转换为 reflect.StringHeader 类型的变量,然后通过操作该变量来修改字符串的内容,示例代码如下:
1) (*reflect.StringHeader)(unsafe.Pointer(&str1)) 把字符串 str1 的指针转换为 unsafe.Pointer 类型后,再把 unsafe.Pointer 类型转换为 StringHeader 的指针,然后通过读、写 str1Header 的成员来读、写字符串 str1 内部的成员。
2) 通过修改 str1Header 中 Data 的值来修改 str1 字节数组的指向。
3) 待字符串的内容修改完以后,为了保证字符串的结果是完整的,还需要通过修改 str1Header 的 Len 值来修改字符串 str1 的长度。最后,str1 的值被修改成 str2 的值,即"hello,Go语言"。
1) 首先获取指向变量 db 的指针,然后使用 unsafe.Pointer 类型将其转换为 *string 并进行赋值操作。由于成员变量 DbName 是结构体类型变量 db 的第一个字段,因此修改 DbName 时不用偏移。
2) 修改结构体类型变量 db 的成员 IsOpensource 的值时,由于 IsOpensource 不是第一个字段,因此需要计算内存地址的偏移量。计算内存地址的偏移量要使用 uintptr 类型,可以先将变量 db 的指针地址转为 uintptr 类型,然后通过 unsafe.Offsetof(db.IsOpensource) 方法获取偏移量,最后再计算地址和偏移量。
3) 偏移后,内存地址已经是 db.IsOpensource 字段的地址,如果要为它赋新值,需要把 uintptr 类型转换为 *bool 类型。这里可以使用 unsafe.Pointer 进行中转,先将 uintptr 类型转换为 unsafe.Pointer 类型,再转换为 *bool 类型。
需要注意的是,代码 (*bool)(unsafe.Pointer(uintptr(unsafe.Pointer(db)) + unsafe.Offsetof(db.IsOpensource))) 很长,可能有读者会按照下面的方式改写:
从垃圾回收器的角度看,unsafe.Pointer 类型是一个指向变量的指针,因此当变量被移动时,对应的指针也会被更新。但是 uintptr 类型的临时变量的值只是一个布尔值,所以不应该被改变。
上面的代码引入了一个非指针的临时变量 offSetCol,所以导致垃圾回收器无法正确地识别它,无法确认它是否是一个指向变量 db 的指针。当执行第二个语句时,变量 db 可能已经被移动。在这种情况下,临时变量 offSetCol 的值也就不再指向 db.IsOpensource 的地址值了。当执行第三个语句时,对无效地址空间赋值就会引发程序 panic!所以,如果涉及以上操作,建议不要拆开分段写。
unsafe.Pointer类型
如果在深入学习数据结构时阅读源码,会发现许多类型的定义中都使用了一种叫作 unsafe.Pointer 的类型。例如,在源码 runtime/slice.go 中,切片的定义如下:type slice struct { array unsafe.Pointer len int cap int }其中,元素 array 的类型就是 unsafe.Pointer。
我们知道,Go 语言中类型之间的转换是显式而非隐式的,并且互相转换的类型要彼此兼容,因为不同的类型之间不能进行赋值操作。示例代码如下:
//转换测试 func TestNormalConvert(t *testing.T) { u := uint(1) i := int(1) fmt.Println(&u, &i) //输出地址 p := &i //p 的类型是 *int p = &u //&u的类型是 *uint,与 p的类型不同,它们之间不能相互赋值 p = (*int)(&u) //这种类型转换的语法也是无效的 fmt.Println(p) }因为类型不同,所以对于赋值操作,在编译时会报错:
.\unsafepointer_test.go:14:4: cannot use &u (type *uint) as type *int in assignment
.\unsafepointer_test.go:15:12: cannot convert &u (type *uint) to type *int
而 unsafe.Pointer 是打破这个限制的特殊类型!示例代码如下:
//使用unsafe.pointer进行强制转换 func TestUnsafePointerConvert(t *testing.T) { u := uint(1) i := int(1) fmt.Println(&u, &i) //输出:0xc00000a3c0 0xc00000a3c8 p := &i p = (*int)(unsafe.Pointer(&u)) fmt.Println(p) //输出:0xc00000a3c0 }
Go语言unsafe包简介
Go 官方文档中说明了内置的 unsafe 包是 Go 程序中与类型及内存安全相关的包,所以它很可能是不可移植的,也可能不兼容 Go1.x 版本。顾名思义,unsafe 是不安全的意思。Go 语言定义这个包名,就是告诉我们尽量不要使用它,如果要使用它,也要万般小心。
虽然这个包不安全,但它也有优点,即能打破 Go 语言的类型和内存安全机制,直接对内存进行读、写操作。有时,因为某些需要,我们会冒险使用该包对内存进行操作。
例如,当处理系统调用时,要求 Go 的结构体和 C 的结构体拥有相同的内存结构,那么此时使用 unsafe 包就是唯一的选项。
在 Go 语言中,指针类型不支持向指针地址添加偏移量的操作。那谁能这么做呢?答案是 uintptr 类型!
uintptr 是 Go 语言的内置类型,是存储指针的整型。uintptr 类型的底层是 int 类型(其字节长度与 int 类型一致),它与 unsafe.Pointer 类型可以相互转换。我们可以先将指针类型转换为 uintptr 类型,待做完地址加减运算后,再将其转换成指针类型,最后通过使用“*”达到取值和修改值的目的。
unsafe.Pointer 类型类似于 C语言中的 void *,它是 Go 语言中各种类型的指针相互转换的桥梁,它既可以让任意类型的指针相互转换,也可以将任意类型的指针转换为 uintptr 类型并进行指针运算。
uintptr 和 unsafe.Pointer 类型的区别如下表所示:
关键区别 | uintptr | unsafe.Pointer |
---|---|---|
类型和用途 | 它是一个无符号整数类型,用于存储指针地址的数值。这种类型主要用于进行低级的指针运算,或者在需要直接处理内存地址时与操作系统或硬件交互。在与硬件交互时,uintptr 提供了一种方式将指针转换为对应的内存地址数值 | 它是 Go 语言中的一个通用指针类型,允许不同类型的指针转换而不改变其值。这种类型主要用于需要绕过类型安全的低级编程场景,例如直接与 C代码交互,或执行那些需要精细化操作指针类型的系统编程任务 |
指针运算 | 可以用于指针运算,如加法或减法。但是,使用 uintptr 进行指针运算要谨慎,因为它可能会导致指针指向无效的内存位置 | 可以用来执行不安全的指针类型转换,但不能直接用于指针运算 |
垃圾回收 | Go 语言的垃圾回收器不会将 uintptr 视为有效的对象引用。即便 uintptr 变量存储了某个对象的内存地址,垃圾回收器也不会把该对象标记为“在使用中”。因此,除非还有其他的指针类型直接引用该对象,否则对象仍可能被垃圾回收器回收 | 如果一个对象仅被 unsafe.Pointer 指针引用,它仍然可以被垃圾回收。这是因为 unsafe.Pointer 本身是为了绕过类型安全机制而设计的,在垃圾回收过程中,它无法保证对象不被回收 |
类型转换 | uintptr 可以将指针值转换为整数。我们可以先将 unsafe.Pointer 转换为 uintptr,在进行一些计算或操作后再将其转换回 unsafe.Pointer | 任何类型的指针都可以转换为 unsafe.Pointer,也可以将 unsafe.Pointer 转换为任何其他类型的指针 |
注意,uintptr 和 intptr 分别是无符号和有符号的指针类型,基于安全考虑,Go 语言不允许这两个指针类型彼此之间进行转换。
Go unsafe包中的函数
unsafe 包具有精巧且强大的功能,它提供了三个函数,相关代码如下:type ArbitraryType int type Pointer *ArbitraryType // Pointer本质上是一个int类型的指针 func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr
1) Sizeof()函数
Sizeof() 函数接受 ArbitraryType 类型的参数,返回一个 uintptr 类型的值。这里的 ArbitraryType 可以表示任何类型,但不用担心,它仅仅是一个占位符,一般不用。Sizeof() 函数用于返回类型所占用的内存大小,这个值仅与类型有关,与类型对应的变量存储的内容大小无关。比如布尔型占用 1 字节,int8 也占用 1 字节。对于不带长度的 int 类型,Sizeof() 函数执行的结果与平台有关。
示例代码如下:
//使用sizeof函数 func TestFuncSizeof(t *testing.T) { fmt.Println(unsafe.Sizeof(true)) //输出:1 fmt.Println(unsafe.Sizeof(int8(0))) //输出:1 fmt.Println(unsafe.Sizeof(int16(10))) //输出:2 fmt.Println(unsafe.Sizeof(int32(10000000))) //输出:4 fmt.Println(unsafe.Sizeof(int64(10000000000000))) //输出:8 fmt.Println(unsafe.Sizeof(int(10000000000000000))) //输出:8 }
2) Offsetof()函数
Offsetof() 函数的返回值是结构体中的成员相对于结构体中内存位置的偏移量,结构体中第一个成员的偏移量是 0。示例代码如下:
type MyStruct struct { i byte j int64 k string } //使用offsetof函数计算偏移量 func TestFuncOffsetof(t *testing.T) { var my MyStruct fmt.Println(unsafe.Offsetof(my.i)) //输出:0 fmt.Println(unsafe.Offsetof(my.j)) //输出:8 fmt.Println(unsafe.Offsetof(my.k)) //输出:16 }成员的偏移量就是该成员在结构体内存中的起始位置(内存位置的索引从 0 开始)。根据偏移量可以定位结构体的成员,进而读、写该成员。
3) Alignof()函数
Alignof() 函数返回传入数据类型的对齐值,这个值也可以称为对齐系数。对齐值是与内存对齐有关的值。合理的内存对齐可以提高内存读、写的性能。示例代码如下:
//测试Alignof对齐 func TestFuncAlignof(t *testing.T) { var m map[string]string var p *int32 fmt.Println(unsafe.Alignof(true)) //输出: 1 fmt.Println(unsafe.Alignof(int8(0))) //输出: 1 fmt.Println(unsafe.Alignof(int16(10))) //输出: 2 fmt.Println(unsafe.Alignof(int64(10000000000000))) //输出: 8 fmt.Println(unsafe.Alignof(float32(0))) //输出: 4 fmt.Println(unsafe.Alignof(string('A'))) //输出: 8 fmt.Println(unsafe.Alignof(m)) //输出: 8 fmt.Println(unsafe.Alignof(p)) //输出: 8 }从上面例子的输出结果中可以看到,对齐值一般是 2^n,最大不会超过 8。
提示,反射包中的函数 reflect.TypeOf(x).Align 也可以获取对齐值,它与 unsafe.Alignof(x) 等价。
Go unsafe包的使用方式
1) 使用 unsafe 包修改字符串的内容
我们之前说过字符串定义好以后就不能被直接修改,而 unsafe 包可以绕过这个限制。它是如何做到的呢?我们先来看看字符串在源码中的数据结构。在 Go语言中,字符串底层的结构体 stringStruct 由一个指向字节数组的 unsafe.Pointer 类型和表示长度的 int 类型组成,而结构体 StringHeader 则是字符串在运行时的表现形式。
需要注意的是,StringHeader 的 Data 字段不足以保证数据引用不会被当作垃圾回收,因此程序必须保留一个单独且指向基础数据的指针。
两个结构体的定义如下:
type stringStruct struct { str unsafe.Pointer len int } type StringHeader struct { Data uintptr Len int }
我们可以使用 unsafe 包中的 unsafe.Pointer 类型把字符串类型的指针转换为 reflect.StringHeader 类型的变量,然后通过操作该变量来修改字符串的内容,示例代码如下:
//使用unsafe包修改字符串的内容的测试 func TestModifyString(t *testing.T) { str1 := "hello world" str1Header := (*reflect.StringHeader)(unsafe.Pointer(&str1)) //输出:str1:hello world, data addr:17927321, len:11 fmt.Printf("str1:%s, data addr:%d, len:%d\n", str1, str1Header.Data, str1Header.Len) str2 := "hello,Go语言" str2Header := (*reflect.StringHeader)(unsafe.Pointer(&str2)) str1Header.Data = str2Header.Data str1Header.Len = str2Header.Len //输出:str1:hello,Go语言, data addr:17930063, len:14 fmt.Printf("str1:%s, data addr:%d, len:%d\n", str1, str1Header.Data, str1Header.Len) }代码说明如下:
1) (*reflect.StringHeader)(unsafe.Pointer(&str1)) 把字符串 str1 的指针转换为 unsafe.Pointer 类型后,再把 unsafe.Pointer 类型转换为 StringHeader 的指针,然后通过读、写 str1Header 的成员来读、写字符串 str1 内部的成员。
2) 通过修改 str1Header 中 Data 的值来修改 str1 字节数组的指向。
3) 待字符串的内容修改完以后,为了保证字符串的结果是完整的,还需要通过修改 str1Header 的 Len 值来修改字符串 str1 的长度。最后,str1 的值被修改成 str2 的值,即"hello,Go语言"。
2) 利用偏移量修改结构体中的数据
以下示例演示用偏移量修改结构体中数据的方法:type MyDB struct { DbName string IsOpensource bool } //修改结构体内部的值 func TestUseUnsafeToModify(t *testing.T) { db := new(MyDB) fmt.Println(*db) dbName := (*string)(unsafe.Pointer(db)) *dbName = "Oracle" //计算偏移量到第二个字段 isOPenFlag := (*bool)(unsafe.Pointer(uintptr(unsafe.Pointer(db)) + unsafe.Offsetof(db.IsOpensource))) *isOPenFlag = true fmt.Println(*db) }代码说明如下:
1) 首先获取指向变量 db 的指针,然后使用 unsafe.Pointer 类型将其转换为 *string 并进行赋值操作。由于成员变量 DbName 是结构体类型变量 db 的第一个字段,因此修改 DbName 时不用偏移。
2) 修改结构体类型变量 db 的成员 IsOpensource 的值时,由于 IsOpensource 不是第一个字段,因此需要计算内存地址的偏移量。计算内存地址的偏移量要使用 uintptr 类型,可以先将变量 db 的指针地址转为 uintptr 类型,然后通过 unsafe.Offsetof(db.IsOpensource) 方法获取偏移量,最后再计算地址和偏移量。
3) 偏移后,内存地址已经是 db.IsOpensource 字段的地址,如果要为它赋新值,需要把 uintptr 类型转换为 *bool 类型。这里可以使用 unsafe.Pointer 进行中转,先将 uintptr 类型转换为 unsafe.Pointer 类型,再转换为 *bool 类型。
需要注意的是,代码 (*bool)(unsafe.Pointer(uintptr(unsafe.Pointer(db)) + unsafe.Offsetof(db.IsOpensource))) 很长,可能有读者会按照下面的方式改写:
offSetCol := uintptr(unsafe.Pointer(db)) + unsafe.Offsetof(db.IsOpensource)) isOPenFlag := (*bool)(unsafe.Pointer(offSetCol)) *isOPenFlag = true这种写法在逻辑上是没有错的,但是,进行垃圾回收时会出现未知的错误。如果这里的临时变量 offSetCol 被当作垃圾回收了,那么会导致内存操作出错,从而引发莫名其妙的错误。有时垃圾回收器会移动一些变量来处理内存碎片,这称为移动垃圾回收。若一个变量被移动,那么所有存储该变量旧地址的指针必须被同时更新为变量移动后的新地址。
从垃圾回收器的角度看,unsafe.Pointer 类型是一个指向变量的指针,因此当变量被移动时,对应的指针也会被更新。但是 uintptr 类型的临时变量的值只是一个布尔值,所以不应该被改变。
上面的代码引入了一个非指针的临时变量 offSetCol,所以导致垃圾回收器无法正确地识别它,无法确认它是否是一个指向变量 db 的指针。当执行第二个语句时,变量 db 可能已经被移动。在这种情况下,临时变量 offSetCol 的值也就不再指向 db.IsOpensource 的地址值了。当执行第三个语句时,对无效地址空间赋值就会引发程序 panic!所以,如果涉及以上操作,建议不要拆开分段写。