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 关键字时也使用值传递,但是因为拷贝的是函数指针,所以会产生闭包,并打印出预期的结果。