GO非类型安全指针
unsafe.Pointer简介
- 类似C语言中的无类型指针void*
- 借助unsafe.Pointer有时候可以挽回Go运行时(Go runtime)为了安全而牺牲的一些性能
- 必须小心按照官方文档中的说明使用unsafe.Pointer。稍有不慎,将使Go类型系统(不包括非类型安全指针部分)精心设立内存安全壁垒的努力前功尽弃
- 使用了unsafe.Pointer的代码不受Go 1兼容性保证
unsafe.Pointer相关类型转换编译规则
- 一个类型安全指针可以被显式转换为一个非类型安全指针类型(unsafe.Pointer),反之亦然。
- 一个uintptr值可以被显式转换为一个非类型安全指针类型,反之亦然。
- 注意:这些规则是编译器接收的规则。满足这些规则的代码编译没问题,但并不意味着在运行的时候是安全的。在使用费类型安全指针时,必须遵循一些原则防止不安全的情况发生。
使用unsafe.Pointer的基本运行时原则
- 保证要使用的值在unsafe操作前后时时刻刻要被有效的指针引用着,无论类型安全指针还是非类型安全指针。否则此值可能被垃圾回收器回收掉。
- 任何指针都不应该引用未知内存块。
非类型安全指针相关的事实
- 非类型安全指针值是指针但uintptr值是整数。整数从不引用其他值。
- 不再被使用的内存块的回收时间点是不确定的。
- 某些值的地址在程序运行中可能改变。
- 一个值的生命范围可能并没有代码中看上去的大。
- *unsafe.Pointer是一个类型安全指针类型,它的基类型是unsafe.Pointer。
垃圾回收机制
-
在每一轮垃圾回收过程的开始,所有的内存块将被标记为白色。然后垃圾回收器将所有开辟在栈和全局内存区的内存标记为灰色,并把它们加入一个灰色内存列表。
-
循环直到灰色内存块列表为空:从每个灰色内存块中取出一个内存块,并把它标记为黑色。然后扫描承载此内存块上的指针值,并通过这些指针找到它们引用着的内存块。如果一个引用着的内存块为白色的,则将其标记为灰色并加入灰色内存块列表;否则忽略。
-
最后仍然标记为白色的内存块将被视为垃圾回收掉。
事实一:非类型安全指针值是指针但uintptr值是整数
-
开始执行的时候,先判断一下你对这个表T有没有执行查询的权限。如果有权限,执行器会根据表的引擎定义,去使用这个引擎提供的接口。
-
在慢查询日志中的
rows_examined
字段,表示这个语句执行过程中扫描了多少行,这个值就是在每次调用引擎获取数据行时候累加的,有时候执行器调用一次,引擎内部扫毛了多行,因此引擎扫描行数跟rows_examined
并不是完全相同的。
事实二:不再被使用的内存块的回收时间点是不确定的
启动一轮新的垃圾回收过程的途径:
- GOGC环境变量,runtime/debug.SetGCPercent。
- 调用runtime.GC函数来手动启动。
- 最大垃圾回收时间间隔为两分钟。
#### 事实三:某些值的地址在程序运行中可能改变
- 为了提高性能,每个协程维护着一个栈(一段连续的内存块,64位系统上初始为2k)。在程序运行时,一个协程的栈的大小可能会根据需要而伸缩。当一个栈的大小改变时,runtime需要开辟一段新的连续内存块,并把老的连续内存块上的值复制到新的连续内存块上,从而相应的,开辟在此栈上的指针值中存储的地址可能改变。
- 即:目前开辟在栈上的值的地址可能会改变;开辟在栈上的指针值存储的值可能会自动改变。
事实四:一个值的生命范围可能并没有代码中看上去的大
事实五:*unsafe.Pointer是一个类型安全指针类型,它的基类型是unsafe.Pointer
使用模式一:将类型T1的一个值转换为非类型安全指针值,然后将此非类型安全指针值转换为类型T2(T1的尺寸不小于T2)
func Float64bits(f float64) uint64 {
return *(*uint64)(unsafe.Pointer(&f))
}
func Float64frombits(f uint64) float64 {
return *(*float64)(unsafe.Pointer(&f))
}
字节转字符可以
func ByteSlice2String(bs []byte) string {
return *(*string)(unsafe.Pointer(&bs))
}
但是字符转字节不可以,因为字符没有定义字节数组的长度,可以改为下面的写法
type StringEx struct {
string
cap int
}
func String2ByteSlice(str string) []byte {
se:=StringEx{string:str,cap:len(str)}
return *(*[]byte)(unsafe.Pointer(&se))
}
使用模式二:将一个非类型安全指针值转换为一个uintptr值
func printPointer() {
type T struct {
a int
}
var t T
fmt.Printf("%p\n",&t)
println(&t)
fmt.Printf("%x\n",uintptr(unsafe.Pointer(&t)))
}
使用模式三:将一个非类型安全指针转换为一个uintptr值,然后此uintptr值参与各种算数运算,在将运算结果uintptr值转回非类型安全指针
//转换前后的非类型安全指针(这里的ptr1和ptr2)必须指向同一个内存块
//两次转换必须在同一条语句中
ptr2 = unsafe.Pointer(uintptr(ptr1)+offset)
ptr2 = unsafe.Pointer(uintptr(ptr1)&^7)//8字节对齐
type T struct {
x bool
y [3]int16
}
const N = unsafe.Offsetof(T{}.y)
const M = unsafe.Sizeof(T{}.y[0])
t:=T{y:[3]int16{123,456,789}}
p:=unsafe.Pointer(&t)
ty2:=(*int16)(unsafe.Pointer(uintptr(p)+N+M+M))
fmt.Println(*ty2)//789
使用模式四:需要将reflect.Value.Pointer或者reflect.Value.UnsafeAddr方法的uintptr返回值转换为非类型安全指针
- 设计目的:避免不引用unsafe包就可以将这两个方法的返回值(如果是unsafe.Pointer类型)转换为任何类型安全指针类型
- 不立即转换为unsafe.Pointer,将出现一个可能导致处于返回的地址处的内存块被回收掉的时间窗 `
使用模式五:reflect.SliceHeader或者reflect.StringHeader值的Data字段和非类型安全指针之间的相互转换
- reflect.SliceHeader和切片的内部结构一致;
- reflect.StringHeader和字符串的内部结构一致;
- 使用原则:不要凭空生成SliceHeader和StringHeader,要从切片和字符串转换出它们。
type SliceHeader struct{
Data uintptr
Len int
Cap int
}
type StringHeader struct{
Data uintptr
Len int
}
//编译没问题,也符合基本运行时原则
//但是不推荐这么做,因为打破了对字符串的不变性的与其
//结果字符串不影传递给外部使用
func changeString() {
a:=[...]byte{'G','o','l','a','n','g'}
s:="Java"
hdr:=(*reflect.StringHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(&a))
hdr.Len = len(a)
fmt.Println(s)//Golang
//现在,字符串s和切片a共享着底层的byte字节序列
a[2],a[3],a[4],a[5]='o','g','l','e'
fmt.Println(s)//Goole
}
总结
- 非类型安全机制可以帮助我们写出效率更高的代码
- 但是使用不当,将造成一些重现几率非常低的微妙bug
- 我们应该知晓当前的非类型安全机制规则和使用模式可能在以后的Go版本中完全失效。当然,目前没有任何迹象表明这种变化将很快到来。但是一旦发生这种变化,前面列出的当前是正确的代码将变得不再安全甚至编译不通过