Go语言实践[回顾]教程25--详解Go语言函数的延迟执行调用 defer
在实际项目开发中,在代码逻辑中经常会有涉及到成对的操作,比如打开文件与关闭文件、加锁与解锁、接收请求与回复请求、申请资源与释放资源等。这些操作在开发中常让人忘记后面的操作,只顾写前面的逻辑,忘记关闭与释放资源。如果能有一个方法在打开、申请资源后,就立刻写上关闭、释放资源代码,但不是立刻执行,而是函数执行完其他代码后再执行这些关闭释放代码,那就让开发时针对这类成对出现的操作变得容易很多,BUG率降低很多。Go 语言针对这种情况为我们提供了 defer 关键字。
defer 延迟调用(执行)的定义
defer,注册延迟调用(执行)的关键字,defer 会将其后面的函数或方法调用进行推迟处理,使其延迟执行,延迟到函数临结束前再开始执行。多个 defer 注册将按照最先注册的最后执行的逆向顺序执行。
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
fmt.Println("书写顺序1,未延迟")
defer fmt.Println("书写顺序2,延迟注册1") // 这一行将被最后执行
defer fmt.Println("书写顺序3,延迟注册2")
defer fmt.Println("书写顺序4,延迟注册3")
fmt.Println("书写顺序5,未延迟")
}
上述代码编译执行结果如下:
书写顺序1,未延迟
书写顺序5,未延迟
书写顺序4,延迟注册3
书写顺序3,延迟注册2
书写顺序2,延迟注册1
从示例打印输出的结果可以看出,三个被 defer 注册的打印输出语句是在其他所有未延迟注册的语句执行完之后才执行的。三条 defer 注册延迟的语句,先注册延迟的 fmt.Println(“书写顺序2,延迟注册1”) 是最后执行的,而最后注册延迟的 fmt.Println(“书写顺序4,延迟注册3”) 是这三条语句中最先执行的。
注意:
1、函数正常执行到结尾然后开始执行 defer 注册语句,如果函数发生错误宕机(panic)时,也算函数的一种结束,也会触发去执行 defer 注册语句,所以可以用于异常处理,这个后面实践异常错误处理中会详细说明。
2、defer 后面只能是函数调用或匿名函数,不能是语句,否则编译会报错。
在 return 后面的 defer 不会被执行
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
defer fmt.Println("return之前的延迟调用")
fmt.Println("未延迟的")
return
defer fmt.Println("return之后的延迟调用") // 这一行不会被执行
}
上述代码编译执行结果如下:
未延迟的
return之前的延迟调用
示例中第13行的打印输出语句并没有被执行,因为其上一行是 return,defer 还没来得及注册就遇到 return 结束了函数运行。
函数内通过匿名函数延迟执行一段代码块
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
// 主函数,程序入口
func main() {
defer func() {
a := 8
b := 10
fmt.Println("延迟的", a+b)
}()
fmt.Println("未延迟的")
}
上述代码编译执行结果如下:
未延迟的
延迟的 18
示例中第10~14行演示可以将函数内一段代码作为一个整体需要延迟执行的时候,可以使用匿名函数的方法再使用 defer 注册延迟调用,最终实现目的。
defer 后面函数参数是在注册时赋值的
// test01 项目的 main 包,文件名 main.go
package main
import (
"fmt"
)
func abc(i int) {
fmt.Println("函数内输出的:", i)
}
// 主函数,程序入口
func main() {
a := 8
defer abc(a)
a += 10 // 这里对 a 的修改,不会影响延迟运行的函数 abc 的参数值,执行时参数值仍然是修改之前的 8
fmt.Println("函数外输出的:", a)
}
上述代码编译执行结果如下:
函数外输出的: 18
函数内输出的: 8
从示例输出的结果可以验证,abc 函数虽然是最后执行的,但是接到的参数并不是经过第17行修改后的 18,而是在延迟注册时的第16行位置接到的 a 的值 8。所以 defer 后面函数的参数,只以注册行当时变量该有的值赋值给参数的,而不是调用时才去获取实参的值的。
使用建议
一个函数中,有多个条件分支使用 return 结束函数运行的情况下,同时有资源打开需要关闭的情况下,建议使用 defer,在资源打开后直接使用 defer 注册关闭该资源的代码,这样可以避免忘记关闭资源操作。例如打开一个文件,然后读取内容,内容转换处理,然后关闭文件。这个流程就可以改成 打开文件、延迟关闭文件、读取内容、内容转换处理。具体示例在文件操作章节进行具体实践。
defer 不要在循环中使用,也不要对命名返回值变量操作,否则会出现很难理解的意外结果。defer 使用的位置如果不正确,会有可能导致宕机(panic),所以有错误检查的语句的,最好放在错误检查语句之后。
.
.
上一节:Go语言实践[回顾]教程24–详解Go语言函数的签名、变量、匿名