探索一下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 deferreturn的执行顺序

  • 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

posted @ 2021-03-24 17:58  thepoy  阅读(90)  评论(0编辑  收藏  举报