11

Golang Defer 必会知识点

 8 months ago
source link: https://studygolang.com/articles/36454
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

公众号「程序员祝融」,专注于后端技术,尤其是 Golang 及周边生态。

在Go语言中,defer 定义为关键字, 在开发过程中使用非常高频,但是在使用中会有很多问题,在面试中也是高频考察点。今天我们结合案例来聊聊。

1 defer 作用

在 Golang 中,defer 比面向对象语言中的析构函数要强大很多,defer 还有错误捕获、修改函数返回值、资源释放等,defer 会在当前所在函数返回前执行传入的函数。例如:

func CreateUsers(db *gorm.DB) error {
    tx := db.Begin()
    defer tx.Rollback()

    if err := tx.Create(&Users{Name: "祝融"}).Error; err != nil {
        return err
    }

    return tx.Commit().Error
}

注:调用 tx.Commit()之后执行 tx.Rollback()并不会影响已经提交事务。

2 defer 作用域

defer 作用域在当前函数和方法返回之前被调用。例如:

package main

import "fmt"

func main() {
    {
        defer fmt.Println("defer done")
        fmt.Println("code block done")
    }

    fmt.Println("main done...")
}

$ go run main 
code block done
main done
defer done

我们会发现,传入的函数不是在退出代码块的作用域时执行的,defer 只会在当前函数和方法返回之前被调用。

3 defer 执行顺序

在 Golang 中,defer 的执行顺序是采用栈(stack)的方式。当你使用多个 defer 语句时,它们会按照后进先出(LIFO)的顺序执行。在一个函数生命周期内,优先调用后面的 defer 。例如:

package main

import "fmt"

func main() {
    defer funcA()
    defer funcB()
    defer funcC()
}

func funcA() {
    fmt.Println("A")
}

func funcB() {
    fmt.Println("B")
}

func funcC() {
    fmt.Println("C")
}

$ go run main.go
C
B
A

09a6aef16e994932a5e0fcfa51790bad~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2500&h=1004&s=144654&e=png&b=fffdfd

3.1 defer、return 谁先执行?

在 Golang 中,return 比 defer 先执行,例如:

package main

import "fmt"

func deferFunc() int {
    fmt.Println("defer func done")
    return 0
}

func returnFunc() int {
    fmt.Println("return func done")
    return 0
}

func returnAndDefer() int {
    defer deferFunc()
    return returnFunc()
}

func main() {
    returnAndDefer()
}

$ go run main.go
return func done
defer func done

3.2 defer 影响主函数的具名返回值

上面讲了 return 比 defer 先执行。

当主函数有返回值,且返回值有没有名字没有关系,defer 所作用的函数,即 defer 可能会影响主函数返回值。看一个例子:

func main() {
    fmt.Println(deferFuncReturn())
}

func deferFuncReturn() (j int) { // t初始化0且作用域为该函数全域
    i := 1
    defer func() {
       j++
    }()
    return i
}

$ go run main.go
2

在举一个例子对比一下:

func main() {
    fmt.Println(deferFuncReturn())
}
func deferFuncReturn() int {
    i := 1
    defer func() {
        i++
    }()
    return i
}

$ go run main.go
1

结论:当主函数有返回值 ,会在函数初始化时赋值为0,且其作用域为该函数全域,defer 会影响到该返回值。

3.3 defer 偶遇 panic

在 Golang 中,执行过程中遇到 panic 错误时,遍历所有defer,强行 defer 出栈,并执行 defer。在执行过程中,

  • 遇到 recover 捕获异常停止 panic,返回 recover 继续执行
  • 未设置 recover 捕获异常,遍历完 defer 抛出 panic 信息

3.3.2 defer 未捕获 panic

package main

import (
    "fmt"
)

func main() {
    defer_call()

    fmt.Println("main done...")
}

func defer_call() {
    defer func() { 
        fmt.Println("defer func 1") 
    }()
    defer func() { 
        fmt.Println("defer func 2") 
    }()

    // 触发defer出
    panic("error") 

    defer func() {
        fmt.Println("defer func 3: no exec")
    }()
}
$ go run main.go
defer func 2
defer func 1
panic:error
... 堆栈error...

3.3.1 defer 捕获 panic

package main

import (
    "fmt"
)

func main() {
    defer_call()

    fmt.Println("main done...")
}

func defer_call() {

     defer func() { 
        fmt.Println("defer func 1, 捕获异常") 
        if err := recover(); err != nil {
            fmt.Println(err)
        }

    }()
    defer func() { 
        fmt.Println("defer func 2, 没有捕获异常") 
    }()

     // 触发defer出
    panic("error") 

    defer func() {
        fmt.Println("defer func 3: no exec")
    }()

}

$ go run main.go
defer func 2, 没有捕获异常
defer func 1, 捕获异常
error
main done...

总结:从上面可以看出,程序执行中发生了 panic 异常,panic 前的 defer 一定能被执行到,所以我们一般用于关闭资源等,这样一定能保证资源能被关闭,避免一下问题。

3.3.3 defer 中含有 panic

Golang 中,panic 仅会被最后一个 revover 捕获。

package main

import (
    "fmt"
)


func main()  {
    defer func() {
       if err := recover(); err != nil{
           fmt.Println("err:", err)
       }else {
           fmt.Println("fatal")
       }
    }()

    defer func() {
        panic("defer panic2")
    }()

    panic("panic1")
}
$ go run main.go
err: defer panic2

在上面例子中,panic("panic1")先 触发 defer 强制出栈,第一个执行触发 panic("defer panic2)"异常,此时会覆盖前一个异常 panic,最后继续执行 defer, 最终被 recover()捕获住。

3.4 defer 函数嵌套子函数

分析下方代码,有 4 个函数,其中 x 为 1、2、3、4

package main

import "fmt"

func f(x int, y int) int {
    fmt.Println("x:", x)
    return x
}

func main() {
    defer f(1, f(3, 0))
    defer f(2, f(4, 0))
}

先分析下执行顺序,有 2 个 defer,则会产生 2入栈操作,分别是 f1 、f2。

  • f1 入栈时,因为形参 y 是一个函数,则需要执行该函数,故执行一次输出 (x:3)
  • f2 入栈时,因为形参 y 是一个函数,则需要执行该函数,故执行一次输出(x:4)

main 函数执行完后,执行 defer 函数,所有 defer 出栈,所有执行顺序为 f2、f1。所以程序最终输出的结果是:

$ go run main.go
3
4
2
1

一下所有函数传参均为 1。

4.1 defer_fun1

考点:res 的作用域

func defer_fun1(x int) (res int) {
    res = x
    defer func() {
        res += 3
    }()
    return res
}
  • 函数有返回值,res 初始化为 0
  • res = x 则 res = 1,defer 入栈
  • return res 函数结束后,defer 出栈执行 res +3 = 4

最终函数返回结果为 4

4.2 defer_fun2

func defer_fun2(x int) int {
    res := x
    defer func() {
        res += 3
    }()
    return res
}
  • res = x 则 res = 1,defer 入栈
  • return res 程序结束后,此时函数返回值为1 ,但是返回值不是 res。
  • defer 出栈执行 res +3 = 4

最终函数返回结果为 1

4.3 defer_fun3

func defer_fun3(x int) (res int) {
    defer func() {
        res += x
    }()
    return 2
}
  • 函数有返回值,res 初始化为 0
  • x = 1, defer 入栈
  • 函数结束res=2,执行 defer 出栈执行 res +1 = 3

最终函数返回结果为 3

4.4 defer_fun4

func defer_fun4() (res int) {
    t := 1
    defer func(x int) {
        fmt.Println("x:", x)
        fmt.Println("res:", res)
    }(t)
    t = 2
    return 4
}
  • 函数有返回值,res 初始化为 0
  • t 初始化为1
  • defer 入栈,x 作为形参
  • 执行t =2,此时主函数返回 4,则 res = 4
  • 最后 defer 出栈,控制台输出 x: 1 res: 4

最终函数返回结果为 4。

微信.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK