Golang Defer详细指南


当我们开始学习 Go 时,defer 语句可能是我们最先发现非常有趣的事情之一

defer 语句实际上有 3 种类型(截至 Go 1.22,但以后可能会发生变化):

  • open-coded defer
  • heap-allocated defer
  •  stack-allocated defer

每种类型都有不同的性能和最佳使用场景,如果你想优化性能,了解这些类型会很有用。


什么是 defer?
在深入探讨之前,让我们先快速了解一下 defer。

在 Go 中,defer是一个关键字,用于defer某个函数的执行,直到周围函数完成。

func main() {
  defer fmt.Println("hello")
  fmt.Println(
"world")
}

// Output:
// world
// hello

在此代码片段中,defer 语句计划fmt.Println("hello")在函数的最后执行main。因此,fmt.Println("world")会立即调用,并首先打印“world”。

之后,由于我们使用了 defer,因此在main完成之前的最后一步会打印“hello”。

这就像在函数退出之前设置稍后运行的任务一样。

这对于清理操作非常有用,例如关闭数据库连接、释放互斥锁或关闭文件:

func doSomething() error {
  f, err := os.Open("phuong-secrets.txt")
  if err != nil {
    return err
  }
  defer f.Close()

 
// ...
}

上面的代码很好地展示了 defer 的工作原理,但这也是一种糟糕的使用方式。


Defer 被堆叠
defer当您在函数中使用多个语句时,它们将按照“堆栈stack”顺序执行,这意味着最后一个defer函数将首先执行。

func main() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  defer fmt.Println(3)
}

// Output:
// 3
// 2
// 1

  • 每次调用 defer 语句时,都会将该函数添加到当前 goroutine 链接列表的顶部
  • 当函数返回时,它会遍历链接列表并按照上图所示的顺序执行每个函数。

但请记住,它不会执行 goroutine 链表中的所有 defer,它只会运行返回函数中的 defer,因为我们的 defer 链表可能包含来自许多不同函数的许多 defer。

func B() {
  defer fmt.Println(1)
  defer fmt.Println(2)
  A()
}

func A() {
  defer fmt.Println(3)
  defer fmt.Println(4)
}

因此,只执行当前函数(或当前堆栈框架)中的defer函数。

但是有一种典型的情况是,当前 goroutine 中的所有defer函数都会被跟踪和执行,这时就会发生panic。

Defer, Panic 和 Recover
除了编译时错误外,我们还遇到许多运行时错误:除以零(仅限整数)、越界、取消引用零指针等等。这些错误会导致应用程序崩溃。

Panic 是一种停止当前 goroutine 执行、展开堆栈并执行当前 goroutine 中的defer函数的方法,从而导致我们的应用程序崩溃。

为了处理意外错误并防止应用程序崩溃,您可以使用recover defer函数中的函数来重新获得对恐慌 goroutine 的控制权。

func main() {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Recovered:", r)
    }
  }()

  panic(
"This is a panic")
}

// Output:
// Recovered: This is a panic

通常,人们在 panic 中放置一个错误并用 捕获它recover(..),但它可以是任何东西:字符串、整数等。

在上面的例子中,defer函数内部是唯一可以使用的地方recover。让我进一步解释一下。

这里我们可以列出几个错误。我在实际代码中至少见过三个这样的代码片段。

详细点击标题