一、defer解决什么问题?
试想下,我们在一个函数中对临界区执行加锁操作,但是,临界区的代码可能会异常退出(如下图所示)。
为了防止这种情况,我们需要在每个err后面,都加上一个l.Unlock
,这样的代码也难看了。为了解决这个问题,就需要这样的一个功能:能在函数、方法执行完毕后,进行一些收尾操作,这些操作可能是释放资源、或捕获panic错误,所以就有了defer这个关键字;
二、defer注意点
关于defer源码方面的信息,我觉得了解以下两点即可:
- 知道defer函数会包装成一个
_defer
结构,挂载到相应的协程上; - 知道看汇编代码,找到defer的运行时函数
runtime.deferreturn
;
**【注】**没必要陷入看defer源码上实现上,了解以下这部分,就够了。如果开发过程中,有需求、有精力再去深究细节。
预参数计算
预参数计算,就是说再运行到defer函数的时候,它的参数就已经确定了。像下面这个例子打印出来的i为0;
go
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
LILO
go
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
LILO,表示的是当运行到defer的时候,runtime
将defer
函数打包成一个_defer
挂在到当前协程的_defer
字段。对于上述代码,goroutine的图如下所示。当函数执行完时,从后到前执行对应链表,4,3,2,1
修改函数具名返回值
go
func c() (i int) {
defer func() { i++ }()
return 1
}
上述代码,输出2。原因是,函数返回的1,先复制具名函数i,然后执行i++。「这部分有疑问,可以看下Golang函数布局」
三、defer性能损耗
defer带来方便的同时,也会带来一定的性能损耗。这个我们可以通过Benchmark
来验证下。我们只需要了解有损耗即可,这点损耗对于大部分业务来说是没有影响的;
go
func sum(max int) int {
total := 0
for i := 0; i < max; i++ {
total += i
}
return total
}
func fooWithDefer() {
defer func() {
sum(10)
}()
}
func fooWithOutDefer() {
sum(10)
}
func BenchmarkFooWithDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
fooWithDefer()
}
}
func BenchmarkFooWithOutDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
fooWithOutDefer()
}
}
go
➜ defer git:(master) ✗ go test -bench .
goos: darwin
goarch: amd64
pkg: go-tool/basic/defer
BenchmarkFooWithDefer-12 172309779 6.91 ns/op
BenchmarkFooWithOutDefer-12 249897656 4.76 ns/op
PASS
ok go-tool/basic/defer 4.080s
总结
本文主要描述关于defer对的以下几点:
- defer解决什么问题?
- defer三个重要的特性,分别是预参数计算、LILO、修改函数具名返回值;
- 通过
Benchmark
了解defer
带来的性能损耗;