Go标准库
Go标准库中的unsafe包非常简洁,如下所示
1 |
// 用于获取一个表达式值的大小 |
典型使用
怎么理解Go核心团队在尽力保证go类型安全的情况下,又提供了可以打破安全屏障的unsafe.Pointer
这一行为?
首先被广泛应用于Go标准库和Go 运行时的实现当中,reflect、sync、syscall、runtime都是unsafe包的重度用户。
reflect
ValueOf 和TypeOf函数是reflect包中用得最多的两个API,他们是进入运行时反射层、获取发射层信息的入口。这两个函数均将任意类型变量转化为一个interface{}
类型变量,再利用unsafe.Pointer
将这个变量绑定的内存区域重新解释为reflect.emptyInterface类型,以获得传入变量的类型和值类的信息
1 |
// $GOROOT/src/reflect/values.go |
sync
sync.Pool 是个并发安全的高性能临时对象缓冲池。Pool 为每个P分配了一个本地缓冲池,并通过下列函数为每个P分配啦一个本地的缓冲池,并通过如下函数实现快速定位P的本地缓冲池。
1 |
func indexLocal(l unsafe.Pointer, i int) *poolLocal { |
标准库中的saycall包封装了与操作系统交互的系统调用接口,比如Statfs、Listen、Select
1 |
// $GOROOT/src/syscall/zsyscall_linux_amd64.go |
这类的高级调用的最终都会落到调用函数下面一系列的Syscall和RawSyscall函数上面。
1 |
func Syscall(trap, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) |
这些Saycall系列的函数接受的参数类型均为uintptr,这样当封装的系统调用的参数为指针类型时(比如上面Select的参数r、w、e等)。只能只能通过unsafe.uintptr值,就像上面Select函数实现中那样。因此,syscall包是unsafe重度使用者,它的实现离不开的unsafe.Pointer。
runtime包中的unsafe包的典型应用
runtime 包实现的goroutine调度和内存管理(包括GC)都有unsafe包的身影以goroutine的栈管理为例:
1 |
// $GOROOT/src/runtime/stack.go |
unsafe.Pointer 与 uintptr
作为Go类型安全层上的一个“后门”,unsafe包在带来强大的低级编程能力的同时,也极容易导致代码出现错误。而出现浙西错误的原因主要就是对unsafe.Pointer和uintptr的理解不到位。因此正确理解unsafe.Pointer和uintptr对于安全使用unsafe包非常有必要。
Go语言内存管理是基于垃圾回收的,垃圾回收会定期进行,如歌一块内存没有被任何对象引用,他就会被垃圾回收器回收掉,而对象引用是通过指针实现的。
unsafe.Pointer 和其他常规类型的指针一样,可以作为对象引用。如果一个对象仍然被某个unsafe.Pointer 变量引用着则该对象不会被回收(垃圾回收)。
即使他存储的是某个对象的内存地址值,它也不会被算作该对象的被算作对该对象的引用
如果认为将对象地址存储在一个uintptr变量中,该对象就不会被垃圾回收器回收,那是对uintptr的最大误解
安全使用
我们既需要unsafe.Pointer打破类型的安全屏障,又需要器能够被安全的使用。如下有几条安全法则
*T1
-> unsafe.Pointer -> *T2
其本质就是内存重解释,将原本解释为T1的类型内存重新解释为T2类型。
这是unsafe.Pointer 突破Go类型安全的屏障的基本使用模式
注意:转换后类型T2, 对对其系数不能比转换前类型T1的对其系数更严格,即Alignof(T1) => Alignof(T2)
unsafe.Pointer -> uintptr
将unsafe.Pointer 显示转换为uinptr,并且转换后的uintptr类型不会在转换回unsafe.Pointer 只用于打印输出,并不参与其他操作。
模拟指针运算
操作任意内存地址上的数据都离不开指针运算。Go常规语法不支持指针运算,但我们可以使用unsafe.Pointer的第三种安全使用模式来模拟指针运算,即在一个表达式中,将unsafe.Pointer转换为uintptr类型,使用uintptr类型的值进行算术运算后,再转换回unsafe.Pointer
经常用于访问结构体内字段或数组中的元素,也常用于实现对某内存对象的步进式检查
注意事项:
- 不要越界offset理论上可以是任意值,这就存在算术运算之后的地址超出原内存对象边界的可能。 经过转换后,p指向的地址已经超出原数组a的边界,访问这块内存区域是有风险的,尤其是当你尝试去修改它的时候。
- unsafe.Pointer -> uintptr -> unsafe.Pointer的转换要在一个表达式中
调用syscall.Syscall系列函数时指针类型到uintptr类型参数的转换
将reflect.Value.Pointer或reflect.Value.UnsafeAddr转换为指针
reflect.SliceHeader和reflect.StringHeader必须通过unsafe.Pointer -> uintptr
构建
Tips:
使用unsfa包前,请牢记并理解unsafe.Pointer 的六条安全使用模式
如果使用了unsafe包,请使用go vet
等工具对代码进行unsafe包使用合规性检查