golang unsafe实践与原理
<h1>指针类型</h1>
关于指针的好处这里就不描述了。golang是一种强类型的语言,golang 的指针多了一些限制。但这也算是 Go 的成功之处:既可以享受指针带来的便利,又避免了指针的危险性。下面主要讲一下golang里面对指针的一些限制: 我们在其余的语言(C 等),我们如果想通过数组指针访问不同的索引的元素,直接怼 ptr 之类的操作就可以。但是在golang里面对指针的任何数学运算都是不允许的。 也就是说一个指针只能指向一种数据类型,即使是golang里面的组合关系,或者是同样是想某个interface的两个struct也不能互相转换。golang的指针很纯粹。 不同类型的指针不能使用 == 或者 != 比较。 不同类型的指针变量不能相互赋值 golang里面的指针是类型安全的,是因为编译器帮我们做了很多的检查工作,这必然会带来性能的损失。 对于高阶程序员有非类型安全的指针,这就是 unsafe 包提供的 unsafe.Pointer。在某些情况下,它会使代码更高效,当然,也更危险。 Pointer 表示指向任意类型的指针。有四种特殊操作可以用于指针类型而不能用于其他类型: uintptr:uintptr 是 Go 的内置类型。返回无符号整数,可存储一个完整的地址,后续常用于指针数学运算。 Pointer 允许程序破坏类型系统并对任意的内存进行读写。使用应非常小心。 unsafe 包用于 Go 编译器,在编译阶段使用。从名字就可以看出来,它是不安全的,官方并不建议使用。 但是高阶的 Gopher,怎么能不会使用 unsafe 包呢?它可以绕过 Go 语言的类型系统,直接操作内存。例如,一般我们不能操作一个结构体的未导出成员,但是通过 unsafe 包就能做到。unsafe 包让我可以直接读写内存,还管你什么导出还是未导出。 Go 语言类型系统是为了安全和效率设计的,有时,安全会导致效率低下。有了 unsafe 包,高阶的程序员就可以利用它绕过类型系统的低效。因此,它就有了存在的意义,阅读 Go 源码,会发现runtime里面有大量使用 unsafe 包的例子。 先看源码里面 unsafe.Pointer 的定义: 从命名来看,Arbitrary 是任意的意思,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 void* unsafe 包还有其他三个函数: Sizeof 返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,对于一个指针,函数返回的大小为 8 字节(64位机上),一个 slice 的大小则为 slice header 的大小。 Offsetof 返回结构体成员在内存中的位置离结构体起始处的字节数,所传参数必须是结构体的成员。 Alignof 返回 m,m 是指当类型进行内存对齐时,它分配到的内存地址能整除 m。 注意到以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行,它们的结果可以直接赋给 const 型变量。另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可移植的。 综上所述,unsafe 包提供了 2 点重要的能力: pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。 还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收( 不要让uintptr变量出现临时变量,不然有被GC的风险)。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。 unsafe 包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 /usr/local/go/src/cmd/compile/internal/gc/unsafe.go 路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。 更深层的原理需要去研究编译器的源码,这里就不去深究了。我们重点关注它的用法,接着往下看。 查看SDK的源码我们可以知道:slice的header的结构是: 调用 make 函数新建一个 slice,底层调用的是 makeslice 函数,返回的是 slice 结构体: 因此我们可以通过 unsafe.Pointer 和 uintptr 进行转换,得到 slice 的字段值。 输出是: Len,cap 的转换流程如下: 先来看一下map的结构: 和 slice 不同的是,makemap 函数返回的是 hmap 的指针,注意是指针: 我们依然能通过 unsafe.Pointer 和 uintptr 进行转换,得到 hamp 字段的值,只不过,现在 count 变成二级指针了: count 的转换过程: 对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。 这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。 看一个例子: 输出结果: i 是结构体的第一个成员,因此可以直接将 &nPointer 解析成 *string。这一点,在前面获取 map 的 count 成员时,用的是同样的原理。 对于结构体的私有成员,我们有办法可以通过 unsafe.Pointer 改变它的值了。通过 unsafe.Sizeof() 函数可以获取成员大小,进而计算出成员的地址,直接修改内存。 这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。 完成这个任务,我们需要了解 slice 和 string 的底层数据结构: 实现: unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。 您可能感兴趣的文章:
<code>func makeslice(et *_type, len, cap int) slice</code>
<code>func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap</code>
golang unsafe实践与原理
想系统学习GO语言(Golang
Go的内存对齐和指针运算详解和实践
深入理解Go——unsafe
go 垃圾回收:三色算法
Golang笔记:语法,并发思想,web开发,Go微服务相关
Golang指针的使用限制和unsafe.Pointer的突破之路
C#中的代理(delegate)
go语言中map的实现原理
为什么要学 Go