首页  >  文章  >  后端开发  >  Golang Defer:堆分配、堆栈分配、开放编码的 Defer

Golang Defer:堆分配、堆栈分配、开放编码的 Defer

WBOY
WBOY原创
2024-08-07 18:10:56538浏览

这是帖子的摘录;完整的帖子可以在这里找到:Golang Defer:从基础到陷阱。

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

但是它还有很多让很多人困惑的地方,并且有许多令人着迷的方面是我们在使用它时经常没有触及的。

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

堆分配、堆栈分配、开放编码延迟

例如,defer 语句实际上有 3 种类型(从 Go 1.22 开始,尽管以后可能会改变):开放编码 defer、堆分配 defer 和堆栈分配。每一种都有不同的性能和不同的最佳使用场景,如果您想优化性能,了解这一点很有帮助。

在本次讨论中,我们将涵盖从基础知识到更高级用法的所有内容,我们甚至会深入研究一些内部细节。

什么是延期?

在我们深入探讨之前,让我们快速浏览一下 defer。

在 Go 中,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 的方式。我们将在下一节中讨论这个问题。

“好吧,很好,但是为什么不把 f.Close() 放在最后呢?”

这样做有几个很好的理由:

  • 我们将关闭操作放在打开附近,这样更容易遵循逻辑并避免忘记关闭文件。我不想向下滚动函数来检查文件是否已关闭;它分散了我对主要逻辑的注意力。
  • 即使发生恐慌(运行时错误),延迟函数也会在函数返回时被调用。

当发生恐慌时,堆栈将被展开,并且延迟函数将按特定顺序执行,我们将在下一节中介绍。

延迟已堆积

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

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

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

每次调用 defer 语句时,都会将该函数添加到当前 goroutine 链表的顶部,如下所示:

Golang Defer: Heap-allocated, Stack-allocated, Open-coded 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)
}

因此,仅执行当前函数(或当前堆栈帧)中的延迟函数。

Golang Defer: Heap-allocated, Stack-allocated, Open-coded Defer

Goroutine 延迟链

但是有一种典型情况,当前 goroutine 中的所有延迟函数都被跟踪并执行,那就是发生恐慌的时候。

延迟、恐慌和恢复

除了编译时错误之外,我们还有很多运行时错误:除以零(仅限整数)、越界、取消引用 nil 指针等等。这些错误会导致应用程序出现恐慌。

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

为了处理意外错误并防止应用程序崩溃,您可以在延迟函数中使用恢复函数来重新获得对恐慌 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

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

In the example above, inside the deferred function is the only place you can use recover. Let me explain this a bit more.

There are a couple of mistakes we could list here. I’ve seen at least three snippets like this in real code.

The first one is, using recover directly as a deferred function:

func main() {
  defer recover()

  panic("This is a panic")
}

The code above still panics, and this is by design of the Go runtime.

The recover function is meant to catch a panic, but it has to be called within a deferred function to work properly.

Behind the scenes, our call to recover is actually the runtime.gorecover, and it checks that the recover call is happening in the right context, specifically from the correct deferred function that was active when the panic occurred.

"Does that mean we can’t use recover in a function inside a deferred function, like this?"

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

func main() {
  defer func() {
    myRecover()
    // ...
  }()

  panic("This is a panic")
}

Exactly, the code above won’t work as you might expect. That’s because recover isn’t called directly from a deferred function but from a nested function.

Now, another mistake is trying to catch a panic from a different goroutine:

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

  go panic("This is a panic")

  time.Sleep(1 * time.Second) // Wait for the goroutine to finish
}

Makes sense, right? We already know that defer chains belong to a specific goroutine. It would be tough if one goroutine could intervene in another to handle the panic since each goroutine has its own stack.

Unfortunately, the only way out in this case is crashing the application if we don’t handle the panic in that goroutine.

Defer arguments, including receiver are immediately evaluated

I've run into this problem before, where old data got pushed to the analytics system, and it was tough to figure out why.

Here’s what I mean:

func pushAnalytic(a int) {
  fmt.Println(a)
}

func main() {
  a := 10
  defer pushAnalytic(a)

  a = 20
}

What do you think the output will be? It's 10, not 20.

That's because when you use the defer statement, it grabs the values right then. This is called "capture by value." So, the value of a that gets sent to pushAnalytic is set to 10 when the defer is scheduled, even though a changes later.

There are two ways to fix this.

...

Full post is available here: Golang Defer: From Basic To Trap.

以上是Golang Defer:堆分配、堆栈分配、开放编码的 Defer的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn