Golang 中的 Defer
在Go语言中,defer
语句用于将一个函数调用推迟到外围函数返回之后执行。它常用于确保某些操作在函数结束时一定会执行,例如资源释放、文件关闭等。
基本语法
defer
语句的基本使用方法如下:
func main() {
defer fmt.Println("World")
fmt.Println("Hello")
}
输出:
Hello
World
在上面的例子中,fmt.Println("World")
在defer
语句中,所以它会在main
函数结束时执行,而不是在定义它的地方立即执行。
多个defer
如果在一个函数中有多个defer
语句,它们的执行顺序是后进先出(LIFO)的。也就是说,最后一个defer
语句会最先执行。
func main() {
defer fmt.Println("First")
defer fmt.Println("Second")
defer fmt.Println("Third")
fmt.Println("Hello")
}
输出:
Hello
Third
Second
First
典型用例
1. 文件操作
在处理文件操作时,可以使用defer
确保文件关闭:
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("example.txt")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// 读取文件内容
}
2. 资源释放
defer
还可以用于释放其它类型的资源,例如网络连接、数据库连接等:
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 数据库操作
}
3. 锁的解锁
在并发编程中,可以使用defer
确保锁在临界区操作完成后被释放:
package main
import (
"fmt"
"sync"
)
var mu sync.Mutex
func main() {
mu.Lock()
defer mu.Unlock()
// 临界区操作
fmt.Println("Critical section")
}
defer与匿名函数
有时候,我们需要在defer
中执行更复杂的操作,此时可以使用匿名函数:
package main
import "fmt"
func main() {
defer func() {
fmt.Println("Deferred call")
}()
fmt.Println("Main function")
}
输出:
Main function
Deferred call
defer与返回值
defer
还可以用来修改返回值:
package main
import "fmt"
func test() (result int) {
defer func() {
result++
}()
return 0
}
func main() {
fmt.Println(test()) // 输出1
}
在上面的例子中,defer
中的匿名函数会在test
函数返回之前执行,并修改result
的值。
defer 误区
以下,我将以对比的形式展开对 defer
误区的展示。(你可以先自己猜一下每段代码的执行结果)
误区一
func Defer() {
i := 0
defer func() {
println(i)
}()
i = 1
}
在这个函数中,defer
语句延迟执行匿名函数,直到 Defer
函数即将返回。
这个匿名函数是一个闭包,它捕获并引用了外部变量 i
。
因此,当 defer
延迟的匿名函数最终执行时,它打印的是闭包捕获的变量 i
的当前值。
- 定义变量
i
并赋值为0
。 - 定义
defer
延迟执行的匿名函数,它捕获变量i
。 - 将变量
i
修改为1
。 Defer
函数即将返回时,执行defer
延迟的匿名函数,打印捕获的变量i
的当前值1
。
因此,Defer
函数的输出是1
。
func DeferV1() {
i := 0
// 立即调用 defer 延迟的匿名函数,并将当前变量 i 的值传递给它的参数 i。
defer func(i int) {
println(i)
}(i)
i = 1
}
在这个函数中,defer
语句延迟执行的匿名函数有一个参数 i
,并且在调用时将当前的 i
值作为参数传递给匿名函数。
这意味着在 defer
声明时,匿名函数参数 i
的值已经确定,并且与外部变量 i
无关。
- 定义变量
i
并赋值为0
。 - 定义
defer
延迟执行的匿名函数,并立即将当前变量i
(值为0
)传递给它的参数i
。 - 将外部变量
i
修改为1
。 DeferV1
函数即将返回时,执行defer
延迟的匿名函数,打印参数i
的值0
。
因此,DeferV1
函数的输出是 0
。
误区二
func DeferReturn() int {
a := 0
defer func() {
a = 1
}()
return a
}
在这个函数中,变量 a
是一个局部变量。
当 return a
语句执行时,defer
语句的执行会在 return
语句之后立即发生,但在实际返回值被传递给调用者之前。
- 定义变量
a
并赋值为0
。 - 设置
defer
延迟执行的匿名函数,将变量a
修改为1
。 - 执行
return a
,此时返回值为0
。 defer
延迟的匿名函数执行,将变量a
修改为1
,但此时返回值已经确定为0
。
因此,DeferReturn
函数的返回值是 0
。
func DeferReturnV1() (a int) {
// 全局可见 命名返回值 a
a = 0
defer func() {
a = 1
}()
return
}
在这个函数中,变量 a
是一个命名返回值。
在 Go 语言中,当一个函数声明了命名返回值时,该返回值变量会在函数开始时被隐式声明,并且在整个函数体中都是可见的。
因此,当 return
语句执行时,返回的值是命名返回值变量 a
的当前值。
- 定义命名返回值变量
a
并隐式初始化为0
。 - 设置
defer
延迟执行的匿名函数,将变量a
修改为1
。 - 执行
return
语句,返回命名返回值变量a
。 - 在实际返回值被传递给调用者之前,执行
defer
延迟的匿名函数,将变量a
修改为1
。
因此,DeferReturnV1
函数的返回值是 1
。
误区三
type MyStruct struct {
name string
}
func DeferReturnV2() *MyStruct {
a := &MyStruct{
name: "ypb",
}
defer func() {
a.name = "zmz"
}()
return a
}
关键点:
- 指针修改
a
是一个指向 MyStruct
实例的指针。
在 defer
延迟的匿名函数中,修改的是 a
指向的对象的 name
字段,而不是指针本身。
这意味着,即使 return a
语句执行时,defer
语句会在实际返回之前执行,修改指针所指向的对象的内容。
- defer 的执行顺序
defer
语句会在包含它的函数返回之前执行。
具体来说,defer
延迟的匿名函数在 return
语句设置返回值之后,实际返回之前执行。
因此,匿名函数在返回之前对指针所指向的对象的修改是有效的。
本例子执行顺序:
- 创建
MyStruct
实例并赋值给a
,此时a.name = "ypb"
。 - 设置
defer
延迟执行的匿名函数,准备在函数返回之前执行。 - 执行
return a
,此时准备返回a
指向的对象。 - 在返回之前,执行
defer
延迟的匿名函数,将a.name
修改为"zmz"
。 - 返回
a
,此时a
指向的MyStruct
实例的name
字段已经被修改为"zmz"
。
defer 自测
只需要自己猜测一下代码的输出结果即可。
很多人误以为在循环中使用 defer
会在每次迭代时执行推迟的操作。实际上,defer
是在函数返回时才执行的,因此在循环中多次使用 defer
会在函数结束时按照后进先出顺序依次执行所有的 defer
语句。
测试一:
// DeferClosureLoop1 函数的输出结果为 十个 10
func DeferClosureLoop1() {
for i := 0; i < 10; i++ {
i := i
defer func() {
println(i)
}()
}
}
测试二:
// DeferClosureLoop2 函数的输出结果为 9 ~ 0
func DeferClosureLoop2() {
for i := 0; i < 10; i++ {
defer func(val int) {
println(val)
}(i)
}
}
测试三:
// DeferClosureLoop3 与 DeferClosureLoop2 函数的输出结果相同
func DeferClosureLoop3() {
for i := 0; i < 10; i++ {
j := i
defer func() {
println(j)
}()
}
}
总结
defer
在 Go 语言中用于推迟函数调用,直到外围函数返回。
常见用法包括确保资源释放(如文件关闭、解锁)、在多层嵌套函数中统一处理异常等。
defer
语句按后进先出顺序执行,支持匿名函数并可修改命名返回值。
需注意变量捕获、循环中使用defer
导致堆积等误区。正确使用defer
有助于代码清晰与资源管理。