defer 的特性
延迟执行
defer 后的函数并不会立即执行,而是推迟到了函数结束后执行。这一特性一般用于资源的释放,例如在加锁之后立即延迟调用解锁的方法,在函数退出时即完成解锁。
var l sync.Mutexl.Lock()defer l.Unlock()延迟执行的特性除了可以用于前面提到的资源释放和异常捕获,有时也用于函数的中间件。
func LoggerMiddleware(next http.HandlerFunc) http.HandlerFunc {return func(w http.ResponseWriter, r *http.Request) { // 记录当前时间 start := time.Now() defer func() { // 计算并记录请求处理的持续时间 log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start)) }() // 调用实际的处理函数 next(w, r)}}在 LoggerMiddleware 函数中,它接受一个 http.HandlerFunc 并返回一个新的 http.HandlerFunc。在返回的处理函数中,由于 defer 定义的匿名函数延迟执行,在调用完实际的请求处理函数之后,匿名函数就能记录请求的处理时间。
参数预计算
defer 的另一个特性是参数的预计算,defer 语句会在声明它的时候立即对其后的函数调用中的参数进行求值。这意味着即使 defer 延迟调用的函数会在最后执行,其参数的值却是在 defer 语句被执行时确定的,这就是所谓的参数预计算。举个例子:
func main() { a := 1 defer func(x int) { fmt.Println(x) }(a + 1) a = 99}由于 defer 语句后面的函数调用会在 defer 语句被执行时立即对其参数进行求值,所以此时会计算表达式 a + 1 的值,又因为 a 的当前值为 1,所以 a + 1 的结果是 2。这个计算结果作为参数 x 传递给匿名函数,即使在 defer 调用后 a 的值发生了变化,传递给 defer 的匿名函数的参数值依然是 2。因此最后输出的值为 2。
现在再来看一个稍微复杂一点的例子:
func a() int { fmt.Println("函数a被执行") return 1}func b() int { fmt.Println("函数b被执行") return 2}func main() { defer fmt.Println("延迟调用的值:", a()) fmt.Println("没有延迟调用的值:", b())}- 首先计算 defer 后面函数的参数,所以需要立即执行函数 a,并将 a 函数的返回结果 1 与前面的字符串进行拼接。
- 然后调用下面的输出语句,也就是没有延迟调用的输出语句,在调用的时候也需要对参数进行拼接,所以 b 函数被执行,并最后返回 2。
- 将前面的字符串与 2 进行拼接,拼接完成后输出即可。
- 最后执行 defer 语句后面的函数。
所以运行结果是:
函数a被执行函数b被执行没有延迟调用的值: 2延迟调用的值: 1LIFO 执行顺序
-
在函数体内部,可能出现多个 defer 函数。这些 defer 函数将按照
后入先出(last-in first-out,LIFO)的顺序执行,这与栈的执行顺序是相同的,或者说定义 defer 类似于入栈操作,执行 defer 类似于出栈操作。 -
资源往往有依赖顺序,比如先申请 A 资源,再跟据 A 资源申请 B 资源,跟据 B 资源申请 C 资源,即申请顺序是:ABC,释放时往往又要反向进行,即释放顺序是:CBA 。每申请到一个用完需要释放的资源时,立即定义一个 defer 来释放资源是个很好的习惯。
defer 返回值陷阱
有一个事实必须要了解,关键字return不是一个原子操作,实际上汇编指令ret只是return操作的一部分。比如语句 return i,实际上分两步进行。第一步将 i 值存入栈中作为返回值,第二步执行跳转。而 defer 的执行时机正是:在返回值存入栈中之后,在ret指令跳转之前。所以说 defer 执行时还是有机会操作返回值的。看两个简单的例子:
func deferFuncReturn() (result int) { i := 1 defer func() { i++ }() return i}func main() { fmt.Println(deferFuncReturn())}- 对于 deferFuncReturn 函数,假如没有中间的 defer ,可以知道返回值是 result ,所以执行顺序是:先将 i 的值赋值给 result ,再将 result 的值保存在栈上,最后返回栈上 result 的值。
- 但是 defer 的执行时机就是等到将 result 变量放在栈上后开始执行,这时候匿名函数将 i 的值从 1 更新为 2,但是最后返回的是 result 的值。所以最后还是返回了 1。
func deferFuncReturn() (result int) { i := 1 defer func() { result++ }() return i}func main() { fmt.Println(deferFuncReturn())}- 对于 deferFuncReturn 函数,假如没有中间的 defer ,可以知道返回值是 result ,所以执行顺序是:先将 i 的值赋值给 result ,再将 result 的值保存在栈上,最后返回栈上 result 的值。
- 但是 defer 的执行时机就是等到将 result 变量放在栈上后开始执行,这时候匿名函数修改了 result 的值。所以,最后返回的是 2。
func deferFuncReturn() (int) { i := 1 defer func() { result++ }() return i}func main() { fmt.Println(deferFuncReturn())}- 这段代码最后还是返回了 1。函数的返回值并不是局部变量 i ,对于匿名返回值来说,可以假定仍然有一个变量存储返回值。由于 defer 的匿名函数只是修改了局部变量 i 的值,并没有修改返回值,所以最后结果还是 1。
defer 的底层原理
defer 数据结构
type _defer struct { heap bool rangefunc bool sp uintptr //函数栈指针 pc uintptr //程序计数器 fn func() //函数地址 link *_defer //指向自身结构的指针,用于链接多个defer head *atomic.Pointer[_defer]}-
defer 后面一定要接一个函数的,所以 defer 的数据结构跟一般函数类似,也有栈地址、程序计数器、函数地址等等。
-
与函数不同的一点是它含有一个指针,可用于指向另一个 defer,每个 goroutine 数据结构中实际上也有一个 defer 指针,该指针指向一个 defer 的单链表,每次声明一个 defer 时就将 defer 插入到单链表表头,每次执行 defer 时就从单链表表头取出一个 defer 执行。