Golang的defer用法

直观理解defer

package main

import (
    "fmt"
)

func main() {

    fmt.Println("defer begin")

    // 将defer放入延迟调用栈
    defer fmt.Println(1)

    defer fmt.Println(2)

    // 最后一个放入, 位于栈顶, 最先调用
    defer fmt.Println(3)

    fmt.Println("defer end")
}

上面的代码将输出

defer begin
defer end
3
2
1

  • 代码的延迟顺序与最终的执行顺序是反向的。
  • 延迟调用是在 defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宕机时。

每次defer语句在执行的时候,都会将函数进行"压栈",函数参数会被拷⻉下来。当外层函数退出时,defer函数会按照定义的顺序逆序执行 。如果defer执行的函数为nil,那么会在最终调用函数中产生panic。

defer要按照定义的顺序逆序执行是因为后面定义的函数可能会依赖前面的资源。如果前面先执行,释放掉这个依赖,那后面的函数就找不到它的依赖了。

defer执行时如何修改变量

defer函数定义时,对外部变量的引用方式有两种,分别是函数参数以及作为闭包引用。在作为函数参数的时候,在defer定义时就把值传递给defer,并被缓存起来。如果是作为闭包引用,则会在defer真正调用的时候,根据整个上下文去确定当前的值。

什么是闭包:Golang基础-闭包 - roadwide - 博客园

  • 例1:

这里就是闭包。

func f() (result int) {
    defer func() {
        result++
    }()
    return 0
}

return xxx这一条语句并不是一条原子指令

含有defer函数的外层函数,返回的过程是这样的:

  • 先给返回值赋值
  • 调用defer函数
  • 返回到更上一级调用函数中

可以用一个简单的转换规则将return xxx改写成

返回值 = xxx

调用defer函数(这里可能会有修改返回值的操作)

return 返回值

所以例1可以改写为

func f() (result int) {
    result = 0
    //在return之前,执行defer函数
    func() {
        result++
    }()
    return
}

返回值是1

  • 例2:
func f() (r int) {
    t := 5
    defer func() {
        t = t + 5
    }()
    return t
}

可以改写为

func f() (r int) {
    t := 5
    //赋值
    r = t
    //在return之前,执行defer函数,defer函数没有对返回值r进行修改,只是修改了变量t
    func() {
        t = t + 5
    }()
    return
}

返回值是5

  • 例3:
func f() (r int) {
    defer func(r int) {
        r = r + 5
    }(r)
    return 1
}

可以改写为

func f() (r int) {
    //给返回值赋值
    r = 1
    /**
     * 这里修改的r是函数形参的值,是外部传进来的
     * func(r int){}里边r的作用域只该func内,修改该值不会改变func外的r值
     */
    func(r int) {
        r = r + 5
    }(r)
    return
}

返回值是1

  • 例4:
func main() {
    i := 0
    defer fmt.Println("a:", i)
    //传参数,不会改变i的值(因为传的是值,不是引用),如上边的例3
    defer func(i int) {
        fmt.Println("b:", i)
    }(i)
    //闭包调用,捕获同作用域下的i进行计算
    defer func() {
        fmt.Println("c:", i)
    }()
    i++
}

输出结果为

c: 1

b: 0

a: 0

defer配合recover

recover(异常捕获)可以让程序在引发panic的时候不会崩溃退出。

在引发panic的时候,panic会停掉当前正在执行的程序,但是,在这之前,它会有序的执行完当前goroutine的defer列表中的语句。

所以我们通常在defer里面挂一个recover,防止程序直接挂掉,类似于try...catch,但绝对不能像try...catch这样使用,因为panic的作用不是为了抓异常。recover函数只在defer的上下文中才有效,如果直接调用recover,会返回nil。

Panic是一个内置的函数: 停止当前控制流, 然后开始panicking. 当F函数调用panic,F函数将停止执行后续的普通语句, 但是之前的defered函数调用仍然被正常执行, 然后再返回到F的调用者. 对于F函数的调用者, F 的行为和直接调用panic函数类似. 以上的处理流程会一直沿着调用栈回朔, 直到 当前的goroutine返回引起程序崩溃! Panics可以通过直接调用panic方式触发, 也可以由某些运行时 错误触发, 例如: 数组的越界访问.

Recover也是一个内置函数: 用于从panicking恢复.Recover和defer配合使用会非常有用. 对于一个普通的执行流程, 调用recover将返回nil, 也没有任何效果。但如果当前goroutine处于panicking状态,recover调用会捕获触发panic时的参数, 并且恢复到正常的执行流程.

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

输出

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Recovered in f 4 的4是产生panic时的调用参数,即g(4)。

注意没有 Returned normally from g. 这一句,panic会波及调用者,使得调用者也停止执行后续语句。

panic传递g(4) --> g(3) --> g(2) --> g(1) --> g(0) --> f(),所以f函数最后一行的打印不执行。f中的recover使得panic不再向上传递,所以f的调用者main函数正常退出。

如果我们从函数f中移除deferred语句,panic在扩散到goroutine栈顶前将不会被捕获, 最终会引起 程序崩溃. 下面是修改后的输出结果

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
panic: 4

panic PC=0x2a9cd8
[stack trace omitted]

参考

Go语言defer(延迟执行语句)

go defer,panic,recover详解 go 的异常处理 - 简书

Defer, Panic, and Recover[翻译] - Go语言中文网 - Golang中文社区

posted @ 2023-03-12 16:02  roadwide  阅读(128)  评论(0编辑  收藏  举报