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版本中完全失效。当然,目前没有任何迹象表明这种变化将很快到来。但是一旦发生这种变化,前面列出的当前是正确的代码将变得不再安全甚至编译不通过