教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 使用 defer 还是不使用 defer?

使用 defer 还是不使用 defer?

发布时间:2022-03-25   编辑:jiaochengji.com
教程集为您提供使用 defer 还是不使用 defer?等资源,欢迎您收藏本站,我们将为您提供最新的使用 defer 还是不使用 defer?资源

对于Go语言的defer语句,或许你回经历一个 赞赏 --> 怀疑 --> 肯定 --> 再怀疑的一个过程,本文带你回顾一下defer的故事,以及如何在代码中使用defer语句。

<h2 id="最初的故事">最初的故事</h2>

Go语言增加的 <code>defer</code> 语句在简化代码方面确实用处多多, 尤其是对资源的释放等场景,提供了简便的代码方法。其实其它语言也有类似的语法或者语法糖, 比如Java就有<code>try-with-resource</code>语句,可以自动释放实现<code>java.io.Closeable</code>的对象。

比如下面的代码:

<figure class="highlight go"><table><tbody><tr><td class="gutter"><pre>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
</pre></td><td class="code"><pre>
<span class="keyword">func</span> foo(bar []<span class="typename">string</span>) {
mu.Lock()
<span class="keyword">defer</span> mu.Unlock()
<span class="keyword">if</span> <span class="built_in">len</span>(bar) ==<span class="number"> 0</span> {
<span class="keyword">return</span>
}
<span class="keyword">for</span> _, s := <span class="keyword">range</span> bar {
<span class="keyword">if</span> !strings.HasPrefix(s, <span class="string">"https://"</span>) {
<span class="keyword">return</span>
}
}
......
}
</pre></td></tr></tbody></table></figure>

如果不使用<code>defer</code>, 代码中可能需要出现多次重复的对同一个资源的清理释放的方法调用。

<figure class="highlight go"><table><tbody><tr><td class="gutter"><pre>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
</pre></td><td class="code"><pre>
<span class="keyword">func</span> foo(bar []<span class="typename">string</span>) {
mu.Lock()
<span class="keyword">if</span> <span class="built_in">len</span>(bar) ==<span class="number"> 0</span> {
mu.Unlock()
<span class="keyword">return</span>
}
<span class="keyword">for</span> _, s := <span class="keyword">range</span> bar {
<span class="keyword">if</span> !strings.HasPrefix(s, <span class="string">"https://"</span>) {
mu.Unlock()
<span class="keyword">return</span>
}
}
......
mu.Unlock()
}
</pre></td></tr></tbody></table></figure>

相比较而言,第一个代码看起来比较好,锁的获取和释放成对出现,没有冗余的代码,锁的延迟释放和锁的获取紧挨着,不会忘记释放锁或者重复释放锁。

所以, 你会在很多Go的项目和库中看到<code>defer</code>的使用,而且在Go的标准库中也大量的使用(在go 1.11.2的标准库中,大约有4400多次的defer调用)。

<h2 id="使用defer是有代价的">使用defer是有代价的</h2>

随着你对Go语言的熟悉,你也许在性能测试中发现defer语句对性能的影响,也许你也阅读过一些文章, 比如雨痕的Go 性能优化技巧 4/10,对defer语句带来的额外开销有一些测试。

下面是对多个defer情况的性能测试:

<figure class="highlight go"><table><tbody><tr><td class="gutter"><pre>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
</pre></td><td class="code"><pre>
<span class="keyword">package</span> test
<span class="keyword">import</span> (
<span class="string">"sync"</span>
<span class="string">"testing"</span>
)
<span class="keyword">var</span> mu sync.Mutex
<span class="comment">//go:noinline</span>
<span class="keyword">func</span> foo() {}
<span class="comment">//go:noinline</span>
<span class="keyword">func</span> deferLockTwo() {
mu.Lock()
<span class="keyword">defer</span> mu.Unlock()
<span class="keyword">defer</span> foo()
}
<span class="comment">//go:noinline</span>
<span class="keyword">func</span> deferLock() {
mu.Lock()
<span class="keyword">defer</span> mu.Unlock()
foo()
}
<span class="comment">//go:noinline</span>
<span class="keyword">func</span> deferLockClosure() {
mu.Lock()
<span class="keyword">defer</span> <span class="keyword">func</span>() { mu.Unlock() }()
foo()
}
<span class="comment">//go:noinline</span>
<span class="keyword">func</span> noDeferLock() {
mu.Lock()
mu.Unlock()
foo()
}
<span class="keyword">func</span> BenchmarkDeferLockTwo(b *testing.B) {
<span class="keyword">for</span> i :=<span class="number"> 0</span>; i < b.N; i {
deferLockTwo()
}
}
<span class="keyword">func</span> BenchmarkDeferLock(b *testing.B) {
<span class="keyword">for</span> i :=<span class="number"> 0</span>; i < b.N; i {
deferLock()
}
}
<span class="keyword">func</span> BenchmarkDeferLockClosure(b *testing.B) {
<span class="keyword">for</span> i :=<span class="number"> 0</span>; i < b.N; i {
deferLockClosure()
}
}
<span class="keyword">func</span> BenchmarkNoDeferLock(b *testing.B) {
<span class="keyword">for</span> i :=<span class="number"> 0</span>; i < b.N; i {
noDeferLock()
}
}
</pre></td></tr></tbody></table></figure>

测试结果:

<figure class="highlight"><table><tbody><tr><td class="gutter"><pre>
1
2
3
4
</pre></td><td class="code"><pre>
BenchmarkDeferLockTwo-4 20000000 89.8 ns/op
BenchmarkDeferLock-4 20000000 70.4 ns/op
BenchmarkDeferLockClosure-4 20000000 67.6 ns/op
BenchmarkNoDeferLock-4 100000000 19.3 ns/op
</pre></td></tr></tbody></table></figure>

可以看到,直接的请求释放锁只需要 19.3纳秒,可是如果通过<code>defer</code>释放锁,却需要70.4纳秒。

比较有意思的是,不通过<code>defer mu.Unlock()</code>,而是通过<code>Closure</code>的方式释放锁,性能会比<code>defer mu.Unlock()</code>好那么一点点。

如果代码中有多个defer, 耗费的时间更长。

可以看到,代码中使用defer, 可能会给程序的性能代码几十纳秒的开销(根据运行环境的不同,数值有所不同)。

当然, 你可以认为,几十纳秒的开销对于我的应用影响不大,一个实际的业务耗费的时间都有100毫秒,所以这个这点时间损耗不算什么。如果实际观察(比如通过pprof trace)defer语句没有影响到你的性能,那么一切还好,但是对于一个负载比较大的机器,对于<code>hot path</code>上的代码,可能需要goroutine竞争的代码,需要对性能进行进一步的优化,还是需要考虑避免对<code>defer</code>滥用。

<blockquote>

hot paths are code execution paths in the compiler in which most of the execution time is spent, and which are potentially executed very often.
by Konrad Rudolph

</blockquote>

当然对于 Mutex 来说, 尽早的释放锁,在临界区结束之后, 而不是在函数返回时才释放锁是我们掌握的一个基本常识, 这样能避免无谓的过长的锁。

不在循环中使用defer也应该是我们掌握的另外一个常识, 因为循环可能产生多个defer语句,性能差,而且defer又会使资源过晚的释放。

Go编译器使用<code>runtime.deferproc</code> 注册延迟调用,除了这个延迟调用的函数地址外,还会复制函数参数,在当前函数返回时,再通过<code>runtime.deferreturn</code>提取相关信息执行延迟调用, 这显然要比直接的一个函数调用指令要麻烦,也难怪性能回下降。 同时,也说明了<code>Closure</code>方式比<code>defer mu.Unlock()</code>性能要好那么一点点,因为<code>Closure</code>方式的延迟函数没有参数。

<h2 id="defer实现的优化和现实">defer实现的优化和现实</h2>

Go 的代码库中也有讨论<code>defer</code>慢的issue, 在2016年曾经热烈讨论过;runtime: defer is slow, 当时有一些项目开始注意这个问题,开始将项目中的一些defer替换成直接锁的释放, 比如prometheus、x/time/rate。

@aclements 对此进行了优化,在 Go 1.8中, defer性能提高了一倍, 当然@aclements承认defer还有优化的空间,但是目前并没有强烈的优化的意愿,除非有测试数据的支持。

@josharian也提供一个case, 他唯一一次的优化是实现一个tiny routines时候,因为涉及到了mutex, 避免过长的竞争所以避免使用<code>defer</code>。

@rhysh 也提供了一个实际的数据,他在实现一个https服务器,可以观察到<code>crypto/tls</code> 和 <code>internal/poll</code>的defer代码回很稳定的占用几个百分点的cpu占用, 至少,香标准库中下面的代码可以进行优化:

<figure class="highlight go"><table><tbody><tr><td class="gutter"><pre>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
</pre></td><td class="code"><pre>
<span class="keyword">func</span> (c *Conn) Write(b []<span class="typename">byte</span>) (<span class="typename">int</span>, error) {
<span class="comment">// interlock with Close below</span>
<span class="keyword">for</span> {
x := atomic.LoadInt32(&c.activeCall)
<span class="keyword">if</span> x<span class="number">&1</span> !=<span class="number"> 0</span> {
<span class="keyword">return</span><span class="number"> 0</span>, errClosed
}
<span class="keyword">if</span> atomic.CompareAndSwapInt32(&c.activeCall, x, x<span class="number"> 2</span>) {
<span class="keyword">defer</span> atomic.AddInt32(&c.activeCall,<span class="number"> -2</span>) <span class="comment">// 这里</span>
<span class="keyword">break</span>
}
}
......
</pre></td></tr></tbody></table></figure>

总的来说, <code>defer</code>不是免费的,但是也不是那么不堪,除非你的代码是是要频繁执行的代码,需要进行进一步的优化,可以考虑去掉defer而采用手工执行, 否则在代码中使用<code>defer</code>并不是一个问题。 从实践上,多观察pprof的监控,看看defer是不是在你的<code>hot path</code>之中。

<h2 id="参考资料">参考资料</h2> <ol><li>https://github.com/golang/go/issues/14939</li> <li>https://medium.com/i0exception/runtime-overhead-of-using-defer-in-go-7140d5c40e32</li> <li>https://blog.learngoprogramming.com/gotchas-of-defer-in-go-1-8d070894cb01</li> <li>https://github.com/golang/go/issues/6980</li> <li>https://github.com/golang/go/issues/20240</li> <li>https://go-review.googlesource.com/c/time/ /29379/5/rate/rate.go</li> <li>https://segmentfault.com/a/1190000005027137</li> </ol>
到此这篇关于“使用 defer 还是不使用 defer?”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Golang Defer详解
golangdefer特性姿势还是有必要了解下的!!!
defer ,panic,recover
Golang中defer关键字实现原理
Golang defer解读
golang中的defer recover panic
16.defer让代码更清晰
defer函数参数求值简要分析
Go 中 defer 的 5 个坑 - 第一部分
golang的defer机制详解

[关闭]
~ ~