• 语句 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

相差几倍的结果足以引起重视。尤其是那些性能要求高且压力大的算法,应避免使用延迟调用。