Go猜想录
大道至简,悟者天成
defer 一个强大的控制流机制

简介

defer 语句是 go 语言独有的一种控制流机制,它会在函数返回时执行(return 语句、执行完函数体、panic,只有这三种场景,而不是退出代码块的作用域或其他的时机)。它可以帮我们轻松实现其他语言中特有的控制流结构。

使用场景

捕获 panic

panic 的用法

  1. panic 只会触发当前 Goroutine 的延迟函数调用
  2. recover 只有在当前的 defer 域中执行才有效
    • 我们的 recover 调用实际上是调用了 runtime.gorecover。它会检查 recover 调用是否发生在正确的上下文中,特别是是否来自发生 panic 时处于活动状态的正确的延迟函数。
  3. 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

defer 修改返回值

例子 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}

通过上面三个小例子,可以得出如果是只能修改带名字的返回值。注意例子 3 中并没有修改 u,而是修改 u 指向的结构体。

三定律

defer 语句的行为是简单且可预测的。它有三个简单的规则,我们在上文中都已覆盖:

  1. A deferred function’s arguments are evaluated when the defer statement is evaluated.
  2. Deferred function calls are executed in Last In First Out order after the surrounding function returns.
  3. Deferred functions may read and assign to the returning function’s named return values.

defer 实现机制

defer 的内部实现分成三种机制:

  • 堆上分配:是指整个 defer 直接分配到堆上,缺点就是要被 GC 管理。
  • 栈上分配:整个 defer 分配到了 goroutine 栈上,不需要被 GC 管理。比堆上分配性能提升 30%。
  • 开放编码 (Open Code):启用内联的优化,相当于把 defer 内容放到了函数最后。启用条件:
    • 函数的 defer 数量少于或者等于 8 个;
      • 用了一个 byte 来记录哪些 defer 要执行。
    • 函数的 defer 关键字不能在循环中执行;
      • 编译的时候不知道有多少个 defer。
    • 函数的 return 语句与 defer 语句的乘积小于或者等于 15 个。

Reference


知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。