- 语句 defer 向当前函数注册稍后执行的函数调用。
- 这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行
- 常用于资源释放、解除锁定,以及错误处理等操作。
注意,延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并缓存起来。如对状态敏感,可改用指针或闭包。
func main() {
x, y := 1, 2
defer func(a int) {
println("defer x, y =", a, y) // y为闭包引用,输出:1 202
}(x) // 注册时复制调用参数
x += 100 // 对x的修改不会影响延迟调用
y += 200
println(x, y) // 101, 202
}
延迟调用可修改当前函数命名返回值,但其自身返回值被抛弃。
多个延迟注册按 FILO 次序执行(栈)。
编译器通过插入额外指令来实现延迟调用执行,而 return 和 panic 语句都会终止当前函数流程,引发延迟调用。另外,return 语句不是 ret 汇编指令,它会先更新返回值。
func test() (z int) {
defer func() {
println("defer:", z) // 100
z += 100 // 修改命名返回值
}()
return 100 // 实际执行次序:z=100, call defer, ret
}
func main() {
println("test:", test()) // 200
}
误用
千万记住,延迟调用在函数结束时才被执行。不合理的使用方式会浪费更多资源,甚至造成逻辑错误。
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()
// do something...
}
}
应该直接调用,或重构为函数,将循环和处理算法分离。
性能
相比直接用 CALL 汇编指令调用函数,延迟调用则须花费更大代价。这其中包括注册、调用等操作,还有额外的缓存开销。
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()
}
}
输出:
BenchmarkCall-4 100000000 22.4 ns/op
BenchmarkDefer-4 20000000 93.8 ns/op
相差几倍的结果足以引起重视。尤其是那些性能要求高且压力大的算法,应避免使用延迟调用。