探索一下defer
Go 语言的 defer
会在当前函数返回前执行传入的函数,它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。
作为一个编程语言中的关键字,defer
的实现一定是由编译器和运行时共同完成的,不过在深入源码分析它的实现之前我们还是需要了解 defer
关键字的常见使用场景以及使用时的注意事项。
使用 defer
的最常见场景是在函数调用结束后完成一些收尾工作,例如在 defer
中回滚数据库的事务:
func createPost(db *gorm.DB) error {
tx := db.Begin()
defer tx.Rollback()
if err := tx.Create(&Post{Author: "Draveness"}).Error; err != nil {
return err
}
return tx.Commit().Error
}
在使用数据库事务时,我们可以使用上面的代码在创建事务后就立刻调用 Rollback 保证事务一定会回滚。哪怕事务真的执行成功了,那么调用 tx.Commit()
之后再执行 tx.Rollback()
也不会影响已经提交的事务。
1 多个defer语句的执行顺序
defer
关键字的插入顺序是从后向前的,而defer
的执行顺序是从前向后的:
func main() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3")
defer fmt.Println("defer 4")
defer fmt.Println("defer 5")
}
在main()
函数中先后写 5 个defer
语句,编绎器内部为每个 Goroutine 维护一个defer
链表,defer
语句的插入顺序是从后向前,所以defer 5
将会被插入到defer
链表的最前面。而defer
链表的执行顺序是从前向后,所以defer 5
语句将优先执行。
2 defer
与return
的执行顺序
- 向
defer
关键字传入的函数会在父函数返回之前执行 return
后的函数先于defer
执行
先看一段简单的示例:
func test() string {
defer fmt.Println("defer is running")
return "test() returns"
}
func main() {
fmt.Println(test())
}
$ go run main.go
defer is running
test() returns
可以看到,test()
函数是先执行了defer
传入的函数,然后返回。
但,如果return
后面是一个有返回值的函数呢?接着看下面的例子:
func returnFunc() string {
fmt.Println("return func")
return "return"
}
func returnAndDefer() string {
defer fmt.Println("defer")
return returnFunc()
}
func main() {
fmt.Println(returnAndDefer())
}
$ go run main.go
return func
defer
return
能够看到,returnAndDefer()
函数是在defer
后的函数执行之后返回的,但return
后的函数却是在defer
之前执行。
3 defer对外部参数的影响
Go 语言中所有的函数调用都是值传递,defer
也不例外。
func test() int {
i := 0
defer func() {
i++
fmt.Println("i in defer: ", i)
}()
return i
}
func main() {
fmt.Println(test())
}
$ go run main.go
i in defer: 1
0
上述运行结果可以看出来,defer
传入的匿名函数内虽然调用了i++
,但并没有改变defer
外部的i
。
4 defer对父函数的有名返回值的影响
有名返回值的作用域是整个函数,包括函数内的函数。所以defer
内对有名返回值的修改会直接修改其指针。
func test() (t int) {
defer func() {
t += 10
}()
return
}
func main() {
fmt.Println(test())
}
$ go run main.go
10
因为defer
后的函数是在父函数返回前执行,所以最后返回的值 \(t\) 会被defer
改变。
但,如果return
返回的值是函数内声明的另一个变量,又会是什么结果?
看下面的例子:
func test() (t int) {
i := 1
defer func() {
t += 10
}()
return i
}
func main() {
fmt.Println(test())
}
test()
函数内定义了一个变量 \(i\) ,并且最后返回的也是这个变量 \(i\) ,而test()
的返回值却是有名变量 \(t\) ,且defer
内也对 \(t\) 进行了修改,那么test()
最后return
的结果是什么?
根据第 \(2\) 点,先执行return
后的函数,再执行defer
后的函数,最后才return
。而上面的示例,代码中虽然写的最后返回的是 \(i\) ,但因为定义函数时已然有了返回值 \(t\) ,所以在最后返回时会执行一个赋值操作t = i
,而这个操作,将会在defer
后的函数前执行,因此defer
中的 \(t\) 已经被赋值为 1 了,所以test()
函数最终的返回值应是 11。
- 上述图片只是示意图,并非真的将
t = i
这个操作写在上图的匿名函数中。
有了上面的推测后,我们执行一下:
$ go run main.go
11
5 参数的预处理/预计算
如果defer
后的函数的参数不是一个值,而是一个有返回值的函数,又是什么样的执行顺序?
如下例:
func test(i int) int {
i++
fmt.Println(i)
return i
}
func main() {
defer test(test(3))
defer test(test(8))
}
执行后的结果:
$ go run main.go
4
9
10
5
如果根据第 \(1\) 点,最后的输入顺序难道不应该是9 10 4 5
吗?为什么实际结果却是4 9 10 5
呢?
是因为调用defer
关键字时会对参数进行预处理/预计算,其中test(3)
和test(8)
并不是在main()
函数退出前计算的,而是在defer
被调用前就已经对是函数的参数进行了预处理,会优先计算这些参数,最后根据预计算的结果执行defer
,因此,会先打印4 9
。