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() 也不会影响已经提交的事务。

作用域

func main() {
    for i := 0; i < 5; i++ {
        defer fmt.Println(i)
    }
}

$ go run main.go
4
3
2
1
0

在函数返回之后,defer开始按照先入后出的顺序执行,由此看出defer的底层是由栈结构实现的。

func main() {
    {
        defer fmt.Println("defer runs")
        fmt.Println("block ends")
    }

    fmt.Println("main ends")
}

$ go run main.go
block ends
main ends
defer runs

这个例子更详细说明了,defer不是在代码块结束之后执行的,而是在函数返回之后执行的。

预计算

Go 语言中所有的函数调用都是传值的,defer 虽然是关键字,但是也继承了这个特性。

func main() {
    for i := 0; i < 5; i++ {
        defer fmt.Println(i)
    }
}

$ go run main.go
4
3
2
1
0

经过分析,我们会发现调用 defer 关键字会立刻对函数中引用的外部参数进行拷贝,所以 i 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出 4 3 2 1 0。

想要解决这个问题的方法非常简单,我们只需要向 defer 关键字传入匿名函数:

func main() {
    for i := 0; i < 5; i++ {
        defer func() { fmt.Println(i) }()
    }
}

$ go run main.go
5
5
5
5
5

虽然调用 defer 关键字时也使用值传递,但是因为拷贝的是函数指针,所以会产生闭包,并打印出预期的结果。