Go解密: defer 的优雅陷阱

上一篇文章中,我们使用的defer 来recover panic,在gopher的实际工作中,defer 就像一个忠诚可靠的队友一样,总是再背后默默的帮我们处理收尾的工作。 比如

1
2
3
4
5
6
7
8
9
10
wg.Add(goroutines)  
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < count/goroutines; j++ {
atomic.AddInt64(&sum, 1)
}
}()
}
wg.Wait()

释放锁,或者其他任何资源。

在Go中,只有在函数和方法内部才能使用defer;defer关键字后面只能接函数或方法,这些函数被称为deferred函数。
defer将它们注册到其所在goroutine用于存放deferred函数的栈数据结构中,这些deferred函数将在执行defer的函数退出前被按后进先出(LIFO)的顺序调度执行

Pasted image 20240229205228
无论是执行到函数体尾部返回,还是在某个错误处理分支显式调用return返回,抑或出现panic,已经存储到deferred函数栈中的函数都会被调度执行。因此,deferred函数是一个在任何情况下都可以为函数进行收尾工作的好场合。

defer 的几个使用场景

  • panic 捕获,因为deferred 再任意场景下都会被执行,所以我们可以再defer中处理异常。(不建议这么做,如果是一般错误,不建议panic,如果必须panic ,就不要捕获)
  • 释放资源
  • 延迟执行,记录函数执行的时间比如
    1
    2
    3
    go func(s time.Time) {
    fmt.Println(time.Now().Sub(s))
    }(time.Now())

defer 带来的性能损耗

defer让进行资源释放(如文件描述符、锁)的过程变得优雅很多,也不易出错。但在性能敏感的程序中,defer带来的性能负担也是Gopher必须知晓和权衡的。
https://gist.github.com/hxzhouh/a07dd587ba52a8afdb07d5655c8ca303

1
2
3
4
5
6
7
8
9
10
11
12
hxzhouh  atomic  ➜ ( main  1)  ♥ 20:16  go test -bench=BenchmarkFooWithDefer 
10000000
goos: darwin
goarch: arm64
pkg: github.com/hxzhouh/go-example/atomic
BenchmarkFooWithDefer-10 189423524 6.353 ns/op
PASS
ok github.com/hxzhouh/go-example/atomic 3.631s
hxzhouh  atomic  ➜ ( main  1)  ♥ 21:05  go test -bench=BenchmarkFooWithoutDefer
BenchmarkFooWithoutDefer-10 273232389 4.397 ns/op
PASS
ok github.com/hxzhouh/go-example/atomic 2.875s

在 go1.12 版本中相同的测试代码,非 defer 版本 要比defer 版本快7倍,经过1.13 1.14 两个大版本的优化,defer 的性能已经显著提高了,在我的电脑上,非defer 版本还是有着50%左右的性能领先。

总结

在多数情况下,我们的程序对性能并不太敏感,我建议尽量使用defer,同时我们需要了解defer 运行的原理,以及几个要避免的地方。