defer 延迟调用

标签:Go语言首次发布:2024-03-24最近修改:2024-04-02

defer 的特性

延迟执行

defer 后的函数并不会立即执行,而是推迟到了函数结束后执行。这一特性一般用于资源的释放,例如在加锁之后立即延迟调用解锁的方法,在函数退出时即完成解锁。

go
var l sync.Mutexl.Lock()defer l.Unlock()

延迟执行的特性除了可以用于前面提到的资源释放和异常捕获,有时也用于函数的中间件。

go
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 语句被执行时确定的,这就是所谓的参数预计算。举个例子:

go
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。

现在再来看一个稍微复杂一点的例子:

go
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 语句后面的函数。

所以运行结果是:

bash
函数a被执行函数b被执行没有延迟调用的值: 2延迟调用的值: 1

LIFO 执行顺序

  • 在函数体内部,可能出现多个 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 执行时还是有机会操作返回值的。看两个简单的例子:

go
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。
go
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。
text
func deferFuncReturn() (int) {    i := 1    defer func() {        result++    }()    return i}func main() {    fmt.Println(deferFuncReturn())}
  • 这段代码最后还是返回了 1。函数的返回值并不是局部变量 i ,对于匿名返回值来说,可以假定仍然有一个变量存储返回值。由于 defer 的匿名函数只是修改了局部变量 i 的值,并没有修改返回值,所以最后结果还是 1。

defer 的底层原理

defer 数据结构

go
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 执行。