介绍
Golang 语言中的 unsafe 包中包含的操作会绕过 Golang 程序的类型安全检查,直接操作内存,从而达到提升性能的目的。导入 unsafe 包可能是不可移植(non-portable)的(随着 Golang 的版本迭代,可能会失效),并且不受 Go 1 兼容性准则的保护,所以我们应该谨慎使用。
本文主要介绍 unsafe 包的 unsafe.Pointer
,它表示任意类型的指针,它类似于 C 语言中的无类型指针 void*
,可以作为指针类型 *T
和 uintptr 类型值之间互相转换的中转站。
我们知道 Golang 语言中的指针类型 *T
,表示一个指向 T 类型变量的指针,因为 Golang 语言是强类型的静态语言,为了安全考虑,规定两个不同的指针类型之间不可以互相转换,比如 *int
不能与 *float64
互相转换。但是,实际上是可以使用 unsafe.Pointer
进行转换。
Golang 语言中的内置数据类型 uintptr
也可以表示任何指针,它实际是数值类型,可以用于存储内存地址。它和 unsafe.Pointer
最大的区别是 unsafe.Pointer
不支持指针运算,比如 +
运算符,但 uintptr
可以支持。以下是 uintptr
的源码:
// uintptr is an integer type that is large enough to hold the bit pattern of // any pointer. type uintptr uintptr
02
unsafe.Ponter
类型
有了前面内容的铺垫,我们开始介绍 unsafe.Ponter
,它表示指向任意类型的指针。以下是 unsafe 包的源码:
// ArbitraryType is here for the purposes of documentation only and is not actually // part of the unsafe package. It represents the type of an arbitrary Go expression. type ArbitraryType int type Pointer *ArbitraryType
unsafe.Ponter
类型有四个转换规则:
- 任何类型的指针值
*T
都可以转换为unsafe.Pointer
。 unsafe.Pointer
可以转换为任何类型的指针值*T
。- uintptr 可以转换为
unsafe.Pointer
。 unsafe.Pointer
可以转换为 uintptr。
unsafe.Pointer
允许程序绕过类型安全检查读写任意内存,所以使用时应格外小心。
unsafe.Pointer
包含 6 个使用模式:
- 使用
unsafe.Pointer
作为中转,将一个指针类型*T
转换为另外一个指针类型*T
。 - 将
unsafe.Pointer
转换为 uintptr(但不返回给unsafe.Pointer
),然后使用 uintptr 值。 - 将
unsafe.Pointer
转换为 uintptr,然后使用 uintptr 值进行算术运算,最后将运算结果 uintptr 值再转换为unsafe.Pointer
。 - 调用
syscall.Syscall
时,将unsafe.Pointer
转换为 uintptr 值,作为参数传递。 - 将
reflect.Value.Pointer
或reflect.Value.UnsafeAddr
的返回结果 uintptr 值,从 uintptr 转换为unsafe.Pointer
。 - 将
reflect.SliceHeader
或reflect.StringHeader
值的 Data 字段与unsafe.Pointer
进行转换。
03
unsafe.Pointer
和 uintptr 使用示例
因为 unsafe.Pointer
不支持运算,所以如果需要指针运算,还需要借助 uintptr 实现。以下示例是通过指针偏移对 struct 结构体中的字段进行指针运算操作,从而找到该字段的内存地址。
package main import ( "fmt" "unsafe" ) type user struct { name string age uint } func main () { // 定义一个指针变量 student := new(user) // user 结构体中的 name 字段是第一个字段,可以直接通过指针修改,不需要使用偏移 studentName := (*string)(unsafe.Pointer(student)) *studentName = "lucy" // user 结构体中的 age 字段不是第一个字段,所以需要使用偏移才能找到 age 字段的内存地址,修改值 studentAge := (*uint)(unsafe.Pointer(uintptr(unsafe.Pointer(student)) + unsafe.Offsetof(student.age))) *studentAge = 18 fmt.Println(*student) }
阅读上面这段代码,我们使用 new 函数定义了一个 *user
类型的指针变量 student,然后使用 unsafe.Pointer
将 *user
类型的指针变量 student 转换为 *string
类型的指针变量 studentName,然后修改 studentName 的值,实际上就是修改 name 字段的值。
因为 age 字段不是 user 结构体的第一个字段,所以需要先使用偏移量找到 age 字段的内存地址,具体操作步骤是:先将 student 指针变量通过 unsafe.Pointer
和 uintptr 转换为 uintptr,然后就可以使用函数 unsafe.Offsetof
计算出 age 字段的偏移量,该函数返回结果也是 uintptr 类型,因为 uintptr 支持运算,最后使用 +
运算符获取 age 字段的内存地址。
找到 age 字段的内存地址之后,还要使用 unsafe.Pointer
转换为 *uint
指针类型,才可以对该内存地址进行读写操作。
该示例主要是为了方便介绍 uintptr 指针运算,没有实际意义。
04
总结
本文介绍了非类型安全指针,它可用于指针类型之间互相转换,但是它绕开了类型安全检查,同时随着 Golang 的版本迭代,unsafe 包可能会失效,并且 unsafe 包不受 Go 1 兼容性准则的保护,所以我们应该谨慎使用。
推荐阅读:
参考资料:
https://golang.org/pkg/unsafe/#Pointer