教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang unsafe实践与原理

golang unsafe实践与原理

发布时间:2022-01-13   编辑:jiaochengji.com
教程集为您提供golang unsafe实践与原理等资源,欢迎您收藏本站,我们将为您提供最新的golang unsafe实践与原理资源
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"/></svg>

<h3>golang unsafe</h3> <ul><li>指针类型</li><li>unsafe</li><li>为什么有 unsafe</li><li>unsafe实现原理</li><li>unsafe 使用实践</li><li><ul><li>获取slice的长度</li><li>获取 map 长度</li><li>Offsetof 获取成员偏移量</li><li>slice和string 零拷贝转换</li></ul></li><li>总结:</li></ul>

<h1>指针类型</h1>

关于指针的好处这里就不描述了。golang是一种强类型的语言,golang 的指针多了一些限制。但这也算是 Go 的成功之处:既可以享受指针带来的便利,又避免了指针的危险性。下面主要讲一下golang里面对指针的一些限制:

<ol><li>指针是不能做数学运算。</li></ol>

我们在其余的语言(C 等),我们如果想通过数组指针访问不同的索引的元素,直接怼 ptr 之类的操作就可以。但是在golang里面对指针的任何数学运算都是不允许的。

<ol start="2"><li>不同类型的指针不能相互转换。</li></ol>

也就是说一个指针只能指向一种数据类型,即使是golang里面的组合关系,或者是同样是想某个interface的两个struct也不能互相转换。golang的指针很纯粹。

<ol start="3"><li>

不同类型的指针不能使用 == 或者 != 比较。

</li><li>

不同类型的指针变量不能相互赋值

</li></ol><h1>
unsafe</h1>

golang里面的指针是类型安全的,是因为编译器帮我们做了很多的检查工作,这必然会带来性能的损失。 对于高阶程序员有非类型安全的指针,这就是 unsafe 包提供的 unsafe.Pointer。在某些情况下,它会使代码更高效,当然,也更危险。

Pointer 表示指向任意类型的指针。有四种特殊操作可以用于指针类型而不能用于其他类型:

<ul><li>任意类型的指针值可以转换为 Pointer</li><li>Pointer 均可转换为任意类型的指针值</li><li>uintptr 均可以转换为 Pointer</li><li>Pointer 均可以转换为 uintptr</li></ul>

uintptr:uintptr 是 Go 的内置类型。返回无符号整数,可存储一个完整的地址,后续常用于指针数学运算。

Pointer 允许程序破坏类型系统并对任意的内存进行读写。使用应非常小心。

unsafe 包用于 Go 编译器,在编译阶段使用。从名字就可以看出来,它是不安全的,官方并不建议使用。

但是高阶的 Gopher,怎么能不会使用 unsafe 包呢?它可以绕过 Go 语言的类型系统,直接操作内存。例如,一般我们不能操作一个结构体的未导出成员,但是通过 unsafe 包就能做到。unsafe 包让我可以直接读写内存,还管你什么导出还是未导出。

<h1>
为什么有 unsafe</h1>

Go 语言类型系统是为了安全和效率设计的,有时,安全会导致效率低下。有了 unsafe 包,高阶的程序员就可以利用它绕过类型系统的低效。因此,它就有了存在的意义,阅读 Go 源码,会发现runtime里面有大量使用 unsafe 包的例子。

<h1>
unsafe实现原理</h1>

先看源码里面 unsafe.Pointer 的定义:

<pre><code class="lang-go hljs"><span class="token keyword">type</span> ArbitraryType <span class="token builtin">int</span> <span class="token keyword">type</span> Pointer <span class="token operator">*</span>ArbitraryType </code></pre>

从命名来看,Arbitrary 是任意的意思,也就是说 Pointer 可以指向任意类型,实际上它类似于 C 语言里的 void*

unsafe 包还有其他三个函数:

<pre><code>func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr </code></pre>

Sizeof 返回类型 x 所占据的字节数,但不包含 x 所指向的内容的大小。例如,对于一个指针,函数返回的大小为 8 字节(64位机上),一个 slice 的大小则为 slice header 的大小。

Offsetof 返回结构体成员在内存中的位置离结构体起始处的字节数,所传参数必须是结构体的成员。

Alignof 返回 m,m 是指当类型进行内存对齐时,它分配到的内存地址能整除 m。

注意到以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行,它们的结果可以直接赋给 const 型变量。另外,因为三个函数执行的结果和操作系统、编译器相关,所以是不可移植的。

综上所述,unsafe 包提供了 2 点重要的能力:

<ol><li>任何类型的指针和 unsafe.Pointer 可以相互转换。</li><li>uintptr 类型和 unsafe.Pointer 可以相互转换。</li></ol>

pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 pointer 类型。

<pre><code class="lang-go hljs"><span class="token comment">// uintptr 是一个整数类型,它足够大,可以存储</span> <span class="token keyword">type</span> <span class="token builtin">uintptr</span> <span class="token builtin">uintptr</span> </code></pre>

还有一点要注意的是,uintptr 并没有指针的语义,意思就是 uintptr 所指向的对象会被 gc 无情地回收( 不要让uintptr变量出现临时变量,不然有被GC的风险)。而 unsafe.Pointer 有指针语义,可以保护它所指向的对象在“有用”的时候不会被垃圾回收。

unsafe 包中的几个函数都是在编译期间执行完毕,毕竟,编译器对内存分配这些操作“了然于胸”。在 /usr/local/go/src/cmd/compile/internal/gc/unsafe.go 路径下,可以看到编译期间 Go 对 unsafe 包中函数的处理。

更深层的原理需要去研究编译器的源码,这里就不去深究了。我们重点关注它的用法,接着往下看。

<h1>
unsafe 使用实践</h1> <h2>获取slice的长度</h2>

查看SDK的源码我们可以知道:slice的header的结构是:

<pre><code>// runtime/slice.go type slice struct { array unsafe.Pointer // 元素存储体指针 len int // 长度 cap int // 容量 } </code></pre>

调用 make 函数新建一个 slice,底层调用的是 makeslice 函数,返回的是 slice 结构体:
<code>func makeslice(et *_type, len, cap int) slice</code>

因此我们可以通过 unsafe.Pointer 和 uintptr 进行转换,得到 slice 的字段值。

<pre><code class="lang-go hljs"><span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span><span class="token string">"----------------slice len-----------------------"</span><span class="token punctuation">)</span> sli <span class="token operator">:=</span> <span class="token function">make</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token builtin">int</span><span class="token punctuation">,</span> <span class="token number">9</span><span class="token punctuation">,</span> <span class="token number">20</span><span class="token punctuation">)</span> slen <span class="token operator">:=</span> <span class="token operator">*</span><span class="token punctuation">(</span><span class="token operator">*</span><span class="token builtin">int</span><span class="token punctuation">)</span><span class="token punctuation">(</span>unsafe<span class="token punctuation">.</span><span class="token function">Pointer</span><span class="token punctuation">(</span><span class="token function">uintptr</span><span class="token punctuation">(</span>unsafe<span class="token punctuation">.</span><span class="token function">Pointer</span><span class="token punctuation">(</span><span class="token operator">&</span>sli<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator"> </span> <span class="token function">uintptr</span><span class="token punctuation">(</span><span class="token number">8</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span>slen<span class="token punctuation">,</span> <span class="token function">len</span><span class="token punctuation">(</span>sli<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment">// 9 9</span> <span class="token keyword">var</span> Cap <span class="token operator">=</span> <span class="token operator">*</span><span class="token punctuation">(</span><span class="token operator">*</span><span class="token builtin">int</span><span class="token punctuation">)</span><span class="token punctuation">(</span>unsafe<span class="token punctuation">.</span><span class="token function">Pointer</span><span class="token punctuation">(</span><span class="token function">uintptr</span><span class="token punctuation">(</span>unsafe<span class="token punctuation">.</span><span class="token function">Pointer</span><span class="token punctuation">(</span><span class="token operator">&</span>sli<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator"> </span> <span class="token function">uintptr</span><span class="token punctuation">(</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span>Cap<span class="token punctuation">,</span> <span class="token function">cap</span><span class="token punctuation">(</span>sli<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment">// 20 20</span> <span class="token punctuation">}</span> </code></pre>

输出是:

<pre><code>----------------slice len----------------------- 9 9 20 20 </code></pre>

Len,cap 的转换流程如下:

<pre><code>Len: &sli => pointer => uintptr => pointer => *int => int Cap: &sli => pointer => uintptr => pointer => *int => int </code></pre> <h2>
获取 map 长度</h2>

先来看一下map的结构:

<pre><code class="lang-go hljs"><span class="token keyword">type</span> hmap <span class="token keyword">struct</span> <span class="token punctuation">{</span> count <span class="token builtin">int</span> flags <span class="token builtin">uint8</span> B <span class="token builtin">uint8</span> noverflow <span class="token builtin">uint16</span> hash0 <span class="token builtin">uint32</span> buckets unsafe<span class="token punctuation">.</span>Pointer oldbuckets unsafe<span class="token punctuation">.</span>Pointer nevacuate <span class="token builtin">uintptr</span> extra <span class="token operator">*</span>mapextra <span class="token punctuation">}</span> </code></pre>

和 slice 不同的是,makemap 函数返回的是 hmap 的指针,注意是指针:
<code>func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap</code>

我们依然能通过 unsafe.Pointer 和 uintptr 进行转换,得到 hamp 字段的值,只不过,现在 count 变成二级指针了:

<pre><code class="lang-go hljs"><span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span><span class="token string">"----------------map len-----------------------"</span><span class="token punctuation">)</span> mp <span class="token operator">:=</span> <span class="token function">make</span><span class="token punctuation">(</span><span class="token keyword">map</span><span class="token punctuation">[</span><span class="token builtin">string</span><span class="token punctuation">]</span><span class="token builtin">int</span><span class="token punctuation">)</span> mp<span class="token punctuation">[</span><span class="token string">"qcrao"</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token number">100</span> mp<span class="token punctuation">[</span><span class="token string">"stefno"</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token number">18</span> lenMap<span class="token operator">:=</span> <span class="token operator">*</span><span class="token operator">*</span><span class="token punctuation">(</span><span class="token operator">*</span><span class="token operator">*</span><span class="token builtin">int</span><span class="token punctuation">)</span><span class="token punctuation">(</span>unsafe<span class="token punctuation">.</span><span class="token function">Pointer</span><span class="token punctuation">(</span><span class="token operator">&</span>mp<span class="token punctuation">)</span><span class="token punctuation">)</span> fmt<span class="token punctuation">.</span><span class="token function">Println</span><span class="token punctuation">(</span>lenMap<span class="token punctuation">,</span> <span class="token function">len</span><span class="token punctuation">(</span>mp<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment">// 2 2</span> <span class="token punctuation">}</span> </code></pre>

count 的转换过程:

<pre><code>&mp => pointer => **int => int </code></pre> <h2>
Offsetof 获取成员偏移量</h2>

对于一个结构体,通过 offset 函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。

这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址。

看一个例子:

<pre><code class="lang-go hljs"><span class="token keyword">type</span> Num <span class="token keyword">struct</span><span class="token punctuation">{</span> i <span class="token builtin">string</span> j <span class="token builtin">int64</span> <span class="token punctuation">}</span> <span class="token keyword">func</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> n <span class="token operator">:=</span> Num<span class="token punctuation">{</span>i<span class="token punctuation">:</span> <span class="token string">"EDDYCJY"</span><span class="token punctuation">,</span> j<span class="token punctuation">:</span> <span class="token number">1</span><span class="token punctuation">}</span> nPointer <span class="token operator">:=</span> unsafe<span class="token punctuation">.</span><span class="token function">Pointer</span><span class="token punctuation">(</span><span class="token operator">&</span>n<span class="token punctuation">)</span> <span class="token comment">// 这种在成员不同包不可见的时候非常管用</span> niPointer <span class="token operator">:=</span> <span class="token punctuation">(</span><span class="token operator">*</span><span class="token builtin">string</span><span class="token punctuation">)</span><span class="token punctuation">(</span>unsafe<span class="token punctuation">.</span><span class="token function">Pointer</span><span class="token punctuation">(</span>nPointer<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">*</span>niPointer <span class="token operator">=</span> <span class="token string">"呵呵"</span> njPointer <span class="token operator">:=</span> <span class="token punctuation">(</span><span class="token operator">*</span><span class="token builtin">int64</span><span class="token punctuation">)</span><span class="token punctuation">(</span>unsafe<span class="token punctuation">.</span><span class="token function">Pointer</span><span class="token punctuation">(</span><span class="token function">uintptr</span><span class="token punctuation">(</span>nPointer<span class="token punctuation">)</span> <span class="token operator"> </span> unsafe<span class="token punctuation">.</span><span class="token function">Offsetof</span><span class="token punctuation">(</span>n<span class="token punctuation">.</span>j<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token operator">*</span>njPointer <span class="token operator">=</span> <span class="token number">2</span> fmt<span class="token punctuation">.</span><span class="token function">Printf</span><span class="token punctuation">(</span><span class="token string">"n.i: %s, n.j: %d\n"</span><span class="token punctuation">,</span> n<span class="token punctuation">.</span>i<span class="token punctuation">,</span> n<span class="token punctuation">.</span>j<span class="token punctuation">)</span> <span class="token punctuation">}</span> </code></pre>

输出结果:

<pre><code>----------------修改私有成员----------------------- n.i: 呵呵, n.j: 2 </code></pre>

i 是结构体的第一个成员,因此可以直接将 &nPointer 解析成 *string。这一点,在前面获取 map 的 count 成员时,用的是同样的原理。

对于结构体的私有成员,我们有办法可以通过 unsafe.Pointer 改变它的值了。通过 unsafe.Sizeof() 函数可以获取成员大小,进而计算出成员的地址,直接修改内存。

<h2>
slice和string 零拷贝转换</h2>

这是一个非常精典的例子。实现字符串和 bytes 切片之间的转换,要求是 zero-copy。想一下,一般的做法,都需要遍历字符串或 bytes 切片,再挨个赋值。

完成这个任务,我们需要了解 slice 和 string 的底层数据结构:

<pre><code class="lang-go hljs"><span class="token keyword">type</span> StringHeader <span class="token keyword">struct</span> <span class="token punctuation">{</span> Data <span class="token builtin">uintptr</span> Len <span class="token builtin">int</span> <span class="token punctuation">}</span> <span class="token keyword">type</span> SliceHeader <span class="token keyword">struct</span> <span class="token punctuation">{</span> Data <span class="token builtin">uintptr</span> Len <span class="token builtin">int</span> Cap <span class="token builtin">int</span> <span class="token punctuation">}</span> </code></pre>

实现:

<pre><code> func stringtobyte(s string) []byte { stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) bh := reflect.SliceHeader{ Data: stringHeader.Data, Len: stringHeader.Len, Cap: stringHeader.Len, } runtime.KeepAlive(&s) return *(*[]byte)(unsafe.Pointer(&bh)) } func bytes2string(b []byte) string{ sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b)) sh := reflect.StringHeader{ Data: sliceHeader.Data, Len: sliceHeader.Len, } runtime.KeepAlive(&b) return *(*string)(unsafe.Pointer(&sh)) } </code></pre> <h1>
总结:</h1>

unsafe 包绕过了 Go 的类型系统,达到直接操作内存的目的,使用它有一定的风险性。但是在某些场景下,使用 unsafe 包提供的函数会提升代码的效率,Go 源码中也是大量使用 unsafe 包。

到此这篇关于“golang unsafe实践与原理”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
golang unsafe实践与原理
想系统学习GO语言(Golang
Go的内存对齐和指针运算详解和实践
深入理解Go——unsafe
go 垃圾回收:三色算法
Golang笔记:语法,并发思想,web开发,Go微服务相关
Golang指针的使用限制和unsafe.Pointer的突破之路
C#中的代理(delegate)
go语言中map的实现原理
为什么要学 Go

[关闭]
~ ~