语句 defer 向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定以及错误处理等操作。
延迟调用,注册的是调用,参数在注册时被复制并缓存起来。多个延迟注册按 FILO 的次序执行。
func main() {
x, y := 1, 2
defer func(i int) {
fmt.Println("defer x, y =", i, y)
}(x)
defer func() {
x += 1
y += 1
}()
x += 1
y += 1
fmt.Println(x, y)
}
// 输出:
// 2 3
// defer x, y = 1 4
对延迟调用的不合理使用会浪费更多资源,甚至造成逻辑错误。如下,不恰当的 defer 导致文件关闭时间延长。
func main() {
for i := 0; i < 10000; i ++ {
path := fmt.SPrintf(".log/%d.txt", i)
f, err := os.Open(path)
if err != nil {
log.Println(err)
continue
}
// 在 main 结束时才会执行,延长了逻辑结束时间和 f 的生命周期,消耗更多的内存等资源
defer f.Close()
}
}
应该直接调用,或者重构为函数,将循环和处理算法分离。
func main() {
do := func(n int) {
path := fmt.SPrintf(".log/%d.txt", i)
f, err := os.Open(path)
if err != nil {
log.Println(err)
continue
}
// 在该匿名函数结束时执行,而不是 main
defer f.Close()
}
for i := 0; i < 10000; i ++ {
do(i)
}
}
性能
相比直接用 CALL 汇编指令调用函数,延迟调用需要花费更多代价。这其中包括注册、调用等操作,还有额外的缓存开销。
// deferBenchmark/deferBenchmark_test.go
package deferBenchmark
import (
"sync"
"testing"
)
var m sync.Mutex
func call() {
m.Lock()
m.Unlock()
}
func deferCall() {
m.Lock()
defer m.Unlock()
}
func BenchmarkCall(b *testing.B) {
for i := 0; i < b.N; i ++ {
call()
}
}
func BenchmarkDefer(b *testing.B) {
for i := 0; i < b.N; i ++ {
deferCall()
}
}
测试以上代码:
$ cd path/to/deferBenchmark
$ go test -bench=.
goos: darwin
goarch: amd64
BenchmarkCall-4 100000000 18.7 ns/op
BenchmarkDefer-4 30000000 57.7 ns/op
PASS
ok _/var/gonotes/src/github.com/lwlwilliam/deferBenchmark 3.691s
可以看出,延迟调用跟普通调用的速度相差了几倍,因此,对那些性能要求高且压力大的算法,应避免使用延迟调用。