Golang Defer 必会知识点

webff · · 695 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

> **公众号「程序员祝融」,专注于后端技术,尤其是 Golang 及周边生态。** 在Go语言中,defer 定义为关键字, 在开发过程中使用非常高频,但是在使用中会有很多问题,在面试中也是高频考察点。今天我们结合案例来聊聊。 # 1 defer 作用 在 Golang 中,defer 比面向对象语言中的析构函数要强大很多,defer 还有错误捕获、修改函数返回值、资源释放等,defer 会在当前所在函数返回前执行传入的函数。例如: ```go 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 作用域在当前函数和方法返回之前被调用。例如: ```go 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 。例如: ```go 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 ``` 图解: ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/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 先执行,例如: ```go 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 可能会影响主函数返回值。看一个例子: ```go func main() { fmt.Println(deferFuncReturn()) } func deferFuncReturn() (j int) { // t初始化0且作用域为该函数全域 i := 1 defer func() { j++ }() return i } $ go run main.go 2 ``` 在举一个例子对比一下: ```go 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 ```go 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 ```go 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 捕获。 ```go 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 ```go 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 $ go run main.go 3 4 2 1 ``` # 4 思考 一下所有函数传参均为 1。 ## 4.1 defer_fun1 考点:res 的作用域 ```go 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 ```go 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 ```go 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 ```go 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](https://static.golangjob.cn/230117/e6d7567719c440eb84c31be78d9b8159.png)

有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

695 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传