defer 使用指南
简介
defer 语句是 go 语言独有的一种控制流机制,它会在函数返回时执行(return 语句、执行完函数体、panic,只有这三种场景,而不是退出代码块的作用域或其他的时机)。它可以帮我们轻松实现其他语言中特有的控制流结构。
使用场景
捕获 panic
panic 的用法
- panic 只会触发当前 Goroutine 的延迟函数调用
- recover 只有在当前的 defer 域中执行才有效
- 我们的
recover
调用实际上是调用了runtime.gorecover
。它会检查recover
调用是否发生在正确的上下文中,特别是是否来自发生panic
时处于活动状态的正确的延迟函数。
- 我们的
- panic 可以多次嵌套
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
panic("This is a panic")
}
// Output:
// Recovered: This is a panic
关闭文件描述符
打开成功后可以直接加上关闭的代码,逻辑清晰,不易遗漏。利用了 named return value
可以优雅的写入关闭时可能的错误。
func open() (err error) {
f, err := os.Open("a.txt")
if err != nil {
return err
}
defer func() {
err = errors.Join(err, f.Close())
}()
// logic below
// ...
return nil
}
回滚数据库事务
在使用数据库事务时,我们其实可以在创建事务之后就立刻调用 Rollback 保证事务一定会回滚,哪怕事务真的执行成功了,那么在调用 tx.Commit () 之后再执行 tx.Rollback () 其实也不会影响已经提交的事务。
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
}
解锁资源
var mu sync.Mutex
func demo() {
// 锁定资源
mu.Lock()
// 使用 defer 在函数返回前解锁资源
defer mu.Unlock()
// 执行一些操作
// ...
}
注意事项
执行顺序
lifo,和栈一样。
package main
func main() {
defer println(1)
defer panic(2)
defer println(3)
defer panic(4)
defer panic(5)
}
// Output:
// 3
// 1
// panic: 5
// panic: 4
// panic: 2
值传递:传递参数时会预计算
调用 defer 关键字时会立刻复制函数中引用的外部参数,所以 i 的值在 defer 关键字调用时就已经确定了。
func main() {
i := 0
defer fmt.Println(i)
i++
}
// Output:
// 0
可以通过向 defer 关键字传入匿名函数来解决这个问题。
func main() {
i := 0
defer func() {
fmt.Println(i)
}()
i++
}
// Output:
// 1
修改返回值
例子 1
func main() {
fmt.Println(deferReturn())
}
func deferReturn() int {
a := 0
defer func() {
a = 1
}()
return a
}
// Output:
// 0
例子 2
func main() {
fmt.Println(deferReturn())
}
func deferReturn() (a int) {
a = 0
defer func() {
a = 1
}()
return a
}
// Output:
// 1
例子 3
func main() {
fmt.Println(deferReturn())
}
func deferReturn() *User {
u := &User{
"jack",
}
defer func() {
u.Name = "rose"
}()
return u
}
type User struct {
Name string
}
// Output:
// &{rose}
通过上面三个小例子,可以得出 defer 只能修改带名字的返回值。注意例子 3 中并没有修改 u,而是修改 u 指向的结构体。
三定律
defer
语句的行为是简单且可预测的。它有三个简单的规则,我们在上文中都已覆盖:
- A deferred function’s arguments are evaluated when the defer statement is evaluated.
- Deferred function calls are executed in Last In First Out order after the surrounding function returns.
- Deferred functions may read and assign to the returning function’s named return values.
实现机制
defer 的内部实现分成三种机制:
- 堆上分配:是指整个 defer 直接分配到堆上,缺点就是要被 GC 管理。
- 栈上分配:整个 defer 分配到了 goroutine 栈上,不需要被 GC 管理。比堆上分配性能提升 30%。
- 开放编码 (Open Code):启用内联的优化,相当于把 defer 内容放到了函数最后。启用条件:
- 函数的 defer 数量少于或者等于 8 个;
- 用了一个 byte 来记录哪些 defer 要执行。
- 函数的 defer 关键字不能在循环中执行;
- 编译的时候不知道有多少个 defer。
- 函数的 return 语句与 defer 语句的乘积小于或者等于 15 个。
- 函数的 defer 数量少于或者等于 8 个;
延伸阅读
- https://go.dev/blog/defer-panic-and-recover
- https://go.dev/ref/spec#Defer_statements
- https://go.dev/ref/spec#Handling_panics
- https://victoriametrics.com/blog/defer-in-go/
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。