defer 延迟调用【GO 基础】

news/发布时间2024/5/17 16:26:39

〇、前言

在 Go 语言中,defer 是一种用于延迟调用的关键字。

defer 在 Go 语言中的地位非常重要,它是确保资源正确释放和程序健壮性的关键字。

本文将通过示例对其进行专门的详解。

一、defer 简介

defer 的主要用途是在函数执行完毕之前,确保某个操作被执行

通常用于:资源的释放管理,如关闭文件句柄、释放撕资源、网络连接、数据库连接等。

以下是 defer 的一些关键特性:

  • 延迟执行:defer 后的函数调用不会立即执行,而是会推迟到包含它的函数执行结束时才执行
  • 逆序执行:如果有多个 defer 语句,它们会按照后进先出的顺序执行,即最后一个被 defer 的语句会最先执行
  • 作用域限制:defer 的作用域仅限于包围它的函数,不同于其他语言中的 finally 关键字,后者的作用域在其异常块中
  • 语法简洁:在语法上,defer 与普通的函数调用没有区别,但是它提供了一种优雅的方式来确保资源的释放
  • 资源管理:defer 常用于确保文件、网络连接等资源在使用完毕后被正确关闭,避免资源泄露。

在实际编程中,合理使用 defer 可以有效地提高代码的可读性和可维护性。下面会进行详细介绍。

二、defer 的使用

2.1 多 defer 时遵循先进后出 FILO

这个不难理解,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。

下面是一段示例代码,0~4 依次执行 defer,查看输出:

package mainimport "fmt"func main() {fmt.Println("begin-------------------!")var whatever [5]struct{}for i := range whatever {defer fmt.Println(i)}fmt.Println("end---------------------!")
}

输出结果是从 4 开始,说明先运行的 defer 后输出结果,且主线程先运行完成:

  

2.2 defer 与闭包

先解释一下闭包:它是一种编程概念,允许一个函数访问并操作其外部作用域中的变量。闭包通常出现在函数嵌套的情况下,即一个函数内部定义了另一个函数。内部的这个函数能够访问所在函数的变量和参数,无论外部函数已经执行完毕。这种结构使得内部函数保留对外部函数变量的引用,因此这些变量不会被垃圾回收机制收回

闭包是由函数及其相关的引用环境组合而成的实体,可以理解为函数加上它所引用的外部变量。在Go语言中,所有的闭包都是匿名函数,但不是所有的匿名函数都是闭包。只有当匿名函数捕获了其外部作用域中的变量时,它才成为一个闭包。

官方对 defer 有句解释:

  • Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.
  • 每次执行“defer”语句时,函数值和调用的参数都会像往常一样求值并重新保存,但不会调用实际的函数。

如下一段示例代码,将上一章节的代码 defer 的内容换成一个函数:

package mainimport "fmt"func main() {fmt.Println("begin-------------------!")var whatever [5]struct{}for i := range whatever {defer func() { fmt.Println(i) }()}fmt.Println("end---------------------!")
}

输出结果变成全部为 4,而非 4~0:

  

函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成 4,所以输出全都是 4。

下面将上边示例改良一下,在循环中添加新的变量暂存循环序列值 i

package mainimport "fmt"func main() {fmt.Println("begin-------------------!")var whatever [5]struct{}for i := range whatever {ii := i // 看似多余的赋值,实际上是将公用变量赋值给局部变量defer func() { fmt.Println(ii) }()}fmt.Println("end---------------------!")
}

运行结果:

  

2.3 某个 defer 发生异常,不会影响其他 defer 的正常执行

 如下代码,在 b 和 c 之间的 defer 异常,但不影响 a 和 b 的正常执行:

package mainfunc test(x int) {defer println("a")defer println("b")defer func() {println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。}()defer println("c")
}func main() {test(0)
}

2.4 延迟调用参数在注册时求值或复制,可用闭包 "延迟" 读取

如下代码,在 x 声明之后,将其作为参数传入 defer 函数体,此时 x 被赋值,后续的值变化将不会影响 defer 的输出:

package mainfunc test() {x, y := 10, 20defer func(i int) {println("defer:", i, y) // y 闭包引用}(x) // x 被复制x += 10y += 100println("x =", x, "y =", y)
}func main() {test()
}

2.5 大循环时,用和不用 defer 的性能比较

如下代码,声明两个互斥锁,分别加锁和关锁,一个使用 defer 关锁,在循环次数较大时看下耗时情况:

package mainimport ("fmt""sync""time"
)var lock sync.Mutex
var lock1 sync.Mutexfunc test() {lock.Lock()lock.Unlock()
}func testdefer() {lock1.Lock()defer lock1.Unlock()
}func main() {func() {t1 := time.Now()for i := 0; i < 1000000; i++ {test()}elapsed := time.Since(t1)fmt.Println("test elapsed: ", elapsed)}()func() {t1 := time.Now()for i := 0; i < 1000000; i++ {testdefer()}elapsed := time.Since(t1)fmt.Println("testdefer elapsed: ", elapsed)}()
}

如下图测试结果,在一百万次时,起始差别也不太大:

  

三、defer 陷阱

3.1 执行闭包(Closure)时的特殊取值

一般情况下,defer 执行的代码行,是参数预加载的,也就是取当前行之前的值,之后变量值变化无影响。

但 defer 执行闭包就不一样了,会取之后代码执行完成后变量最终的值。

package mainimport ("errors""fmt"
)func foo(a, b int) (i int, err error) {defer fmt.Printf("first defer err %v\n", err)// 普通的即时调用函数defer func(err error) {fmt.Printf("second defer err %v\n", err)}(err)// 此处为闭包,函数内部引用了外部的变量defer func() {fmt.Printf("third defer err %v\n", err)}()if b == 0 {err = errors.New("divided by zero!")return // 异常直接返回}i = a / breturn
}func main() {foo(2, 0)
}
// 执行结果,第三个 defer 能取到后续代码赋值给 err 的异常信息
third defer err divided by zero!
second defer err <nil>
first defer err <nil>

3.2 return 的使用

如下示例,函数 foo() 返回值是整数变量 i,第一次赋值是在 i=0 位置,在最后的 return 时,进行了第二次赋值,defer 执行的闭包是一直监视着变量 i 值的变化

package mainimport "fmt"func foo() (i int) {i = 0defer func() {fmt.Println("defer-i:", i)}()fmt.Println("return-before-i:", i)return 2
}func main() {foo()
}
// 结果输出,由于 defer 是在全部代码执行完成后(包括 return)才运行
// 因此,defer 最后读取的是 return 赋值给 i 的值 2
return-before-i: 0
defer-i: 2

另外,defer 不可在有 return 分支的后边使用,这样可能造成 defer 为生效,达不到预期效果。这个很好理解,程序走到 return 后,就直接返回了,后边的代码就不再执行了。

3.3 延迟调用的函数为 nil 时引起 panic

如下示例代码,通过 recover() 函数捕捉 panic 信息:

注:recover() 函数的主要作用是在程序发生 panic 时,能够在 defer 语句中捕获到 panic 的信息,并返回导致 panic 的错误值。这样程序可以据此进行相应的处理,而不是直接崩溃退出

package mainimport ("fmt"
)func test() {var run func() = nildefer run()fmt.Println("runs")
}func main() {defer func() {if err := recover(); err != nil {fmt.Println(err)}fmt.Println("end------------!")}()fmt.Println("start----------!")test()
}
// 结果输出,最后的 end 记录可正常输出,说明程序没有异常退出
start----------!
runs
runtime error: invalid memory address or nil pointer dereference
end------------!

3.4 在 for 循环中调用 defer 并非立即执行

如下图中的示例代码,在 for 循环中每次加载 defer,但是并非每次都会执行,而是在 for 循环完成后一块执行:

这些延迟函数会不停地堆积到延迟调用栈中,最终可能会导致一些不可预知的问题。

解决方案有两种,如下:

1)直接在每次循环完成后,去掉 defer 关键字,直接执行 row.Close():

package mainimport ("database/sql""fmt""time"_ "github.com/denisenkom/go-mssqldb" //  go get github.com/denisenkom/go-mssqldb
)func main() {func() {connStr := "server=<服务器地址>;user id=<用户名>;password=<密码>;database=<数据库名>;encrypt=disable"// 连接到SQL Serverdb, err := sql.Open("mssql", connStr)if err != nil {fmt.Println("db,err:", err)return}for {row, err := db.Query("select ID,shujubs from tablename where shujubs like '20240328%'")if err != nil {fmt.Println("row,err:", err)}row.Close()}}()
}

2)将 for 内循环体包含在新的函数中,在函数内使用 defer:

package mainimport ("database/sql""fmt""time"_ "github.com/denisenkom/go-mssqldb" //  go get github.com/denisenkom/go-mssqldb
)func main() {func() {connStr := "server=<服务器地址>;user id=<用户名>;password=<密码>;database=<数据库名>;encrypt=disable"// 连接到SQL Serverdb, err := sql.Open("mssql", connStr)if err != nil {fmt.Println("db,err:", err)return}for {func() {row, err := db.Query("select ID,shujubs from tablename where shujubs like '20240328%'")if err != nil {fmt.Println("row,err:", err)}defer row.Close() // 函数体中代码执行完后,就会执行 defer}()}}()
}

3.5 在用 {} 包裹的代码块中使用 defer 是没有效果的

首先要知道的是,defer 延迟执行是相对于函数的,而非代码块。

如下代码来测试下,在独立代码块中使用 defer:

package mainimport "fmt"func main() {fmt.Println("start----------!"){ // 独立代码块defer func() {fmt.Println("block: defer runs")}()fmt.Println("block: ends")}fmt.Println("main: ends")fmt.Println("end------------!")
}

如下输出结果,代码块中的 defer 内容,并没有在“main: ends”之前执行:

  

 那么下边来优化下:

package mainimport "fmt"func main() {fmt.Println("start----------!")func() { // 将独立代码块,改造成函数defer func() {fmt.Println("block: defer runs")}()fmt.Println("block: ends")}()fmt.Println("main: ends")fmt.Println("end------------!")
}

 

3.6 使用指针对象作为接收者和不用指针的对比

先看下不用指针的情况:

package mainimport "fmt"type Car struct {model string
}func (c Car) PrintModel() { // 不用指针对象fmt.Println(c.model)
}func main() {fmt.Println("start----------!")c := Car{model: "DeLorean DMC-12"}defer c.PrintModel() // 此时就直接将对象 c 的值保存下来c.model = "Chevrolet Impala" // 此处修改对上边已保存的 c 对象无效fmt.Println("end------------!")
}

下面用指针对象试试看:

package mainimport "fmt"type Car struct {model string
}func (c *Car) PrintModel() { // 使用指针对象fmt.Println(c.model)
}func main() {fmt.Println("start----------!")c := Car{model: "DeLorean DMC-12"}defer c.PrintModel()         // 这里只保存了对象 c 的物理地址c.model = "Chevrolet Impala" // 修改对象 c 后,defer 仍可取最新值fmt.Println("end------------!")
}

参考:http://www.topgoer.com/%E5%87%BD%E6%95%B0/%E5%BB%B6%E8%BF%9F%E8%B0%83%E7%94%A8defer.html     https://studygolang.com/articles/12061#commentForm

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ulsteruni.cn/article/81658103.html

如若内容造成侵权/违法违规/事实不符,请联系编程大学网进行投诉反馈email:xxxxxxxx@qq.com,一经查实,立即删除!

相关文章

2024/4/3

1、今天中润稳步上涨,然后涨停,没有一字涨停或者高开低走,因为明天是清明假期,有连续四天的休市时间,因为怕夜长梦多的避险情绪,所以今天资金流出明显,尤其是小盘股和游资,导致今天很多股票炸板 所以假期之前一定要考虑好,除非前景大好,假期的消息预期可控且假期之前…

轻松玩转书生浦语大模型趣味 Demo——day2笔记

本节课有四个任务:学习部署、玩角色扮演的agent项目,玩数学运算agent、玩写作agent 主要学习过程就是跟着视频,复制学习文档里的资料,完成demo的使用。主要目的是熟悉开发平台。 视频: 轻松玩转书生浦语大模型趣味 Demo_哔哩哔哩_bilibili 资料: Tutorial/helloworld/he…

基于深度学习的肿瘤图像检测系统(网页版+YOLOv8/v7/v6/v5代码+训练数据集)

在本博客中,我们深入探讨了基于YOLOv8/v7/v6/v5的肿瘤图像检测系统。核心上,我们采用了最新的YOLOv8技术,并将其与YOLOv7、YOLOv6、YOLOv5算法进行了综合整合和性能指标对比分析。我们详细阐述了当前国内外在此领域的研究现状、数据集的处理方法、算法的原理、模型构建过程以…

telegram注册自己的bot,并在PC端调试代码

TON 的调试,请在TON环境运行,不要在PC H5里跑,不然一些问题是发现不了的。自己注册个bot,建个mini app,URL指向自己本机安装工具TWA 就是 Telegram Web App tg小程序https://core.telegram.org/bots/webapps#testing-mini-appsPC端调试,需要下载Beta版本的TG 创建自己的b…

针对postgresql已经存在数据,对字段进行hash后分表

PostgreSQL分表方案 在实际应用中,我们经常需要对已经存在的数据进行分表处理,以提高查询效率和数据存储的可靠性。本文将介绍如何使用 PostgreSQL 对已存在的数据进行分表处理。 分表方案 对于已经存在的数据,我们可以采用 hash 分表的方案。具体来说,我们可以使用某个字段…

软件测评师(中级)|画控制流图手把手教程

控制流图画法分解,一文教会画软件测评师中级中的各种流程图1.控制流图概念 控制流图(Control Flow Graph, CFG)也叫控制流程图,是一个过程或程序的抽象表现,是用在编译器中的一个抽象数据结构,由编译器在内部维护,代表了一个程序执行过程中会遍历到的所有路径。它用图的形…