延迟调用(defer)确实是一种 “优雅” 机制。可简化代码,并确保即便发生 panic 依然会被执行。如将 panic/recover 比作 try/except,那么 defer 似乎可看做 finally。
如同异常保护被滥用一样,defer 被无限制使用的例子比比皆是。
![defer](http://studygolang.qiniudn.com/160601/8732956ae01f52d537ffe21d6c39783d.jpg)
![defer_result](http://studygolang.qiniudn.com/160601/bcc28af226bcc78018b77cd9551037ee.jpg)
只需稍稍了解 defer 实现机制,就不难理解会有这样的性能差异。
编译器通过 runtime.deferproc “注册” 延迟调用,除目标函数地址外,还会复制相关参数(包括 receiver)。在函数返回前,执行 runtime.deferreturn 提取相关信息执行延迟调用。这其中的代价自然不是普通函数调用一条 CALL 指令所能比拟的。
![defer2](http://studygolang.qiniudn.com/160601/959442b7fcc933298c48c855ab04792d.png)
![asm](http://studygolang.qiniudn.com/160601/d7cad45e58d5288e06bf452574031317.jpg)
或许你会觉得 4x 的性能差异算不得什么,但如果是下面这样呢?
![download](http://studygolang.qiniudn.com/160601/c47099a8dda6a77fca56201d4c23617e.png)
当多个 goroutine 执行该函数时,只怕性能差异就不是 4x,还得算上 httpGet 所需时间。原本的并发设计,因为错误的 defer 调用变成 “串行”。
与之类似的,还有下面这样的写法。
![files](http://studygolang.qiniudn.com/160601/e898c7bc18c63db4a66c29db61b52c31.jpg)
如果 files 是个 “超大” 列表,只怕在 analysis 结束前,会有不小的隐式 “资源泄露”,这些不能及时回收的对象,会导致 GC 在内的相关性能问题。
解决方法么,要么去掉 f.close 前的 defer,要么将内层处理逻辑重构为独立函数(比如匿名函数调用)。
![files2](http://studygolang.qiniudn.com/160601/9e0f87aeddad7eb64e71fc2e9603d0b1.jpg)
除此之外,单个函数里过多的 defer 调用可尝试合并。最起码,在并发竞争激烈时,mutex.Unlock 不应该使用 defer,而应尽快执行,仅保护最短的代码片段。
有疑问加站长微信联系(非本文作者)