什么是 Defer?

Defer 语句用于存在 defer 语句的函数返回之前执行函数调用。定义可能看起来很复杂,但通过一个例子来理解它很简单。

Example


  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func finished() {
  6. fmt.Println("Finished finding largest")
  7. }
  8. func largest(nums []int) {
  9. defer finished()
  10. fmt.Println("Started finding largest")
  11. max := nums[0]
  12. for _, v := range nums {
  13. if v > max {
  14. max = v
  15. }
  16. }
  17. fmt.Println("Largest number in", nums, "is", max)
  18. }
  19. func main() {
  20. nums := []int{78, 109, 2, 563, 300}
  21. largest(nums)
  22. }

Run in playground

以上是一个简单的程序,用于查找给定切片的最大数量。largest 函数将 int 切片作为参数,并输出输入切片的最大数字。largest 函数的第一行包含语句defer finished()。这意味着在 largest 函数返回之前将调用 finished() 函数。运行此程序,你可以看到以下输出。

  1. Started finding largest
  2. Largest number in [78 109 2 563 300] is 563
  3. Finished finding largest

largest 函数开始执行并输出上述输入的前两行。在它可以返回之前,我们的延迟函数完成执行并输出 Finished finding largest:)

延迟方法


Defer 不仅限于函数。调用延迟方法也是完全合法的。让我们编写一个小程序来测试它。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. type person struct {
  6. firstName string
  7. lastName string
  8. }
  9. func (p person) fullName() {
  10. fmt.Printf("%s %s",p.firstName,p.lastName)
  11. }
  12. func main() {
  13. p := person {
  14. firstName: "John",
  15. lastName: "Smith",
  16. }
  17. defer p.fullName()
  18. fmt.Printf("Welcome ")
  19. }

Run in playground

在上面的程序中我们在 21 行调用了延迟方法,其余代码都一目了然。程序输出

  1. Welcome John Smith


参数评估


延迟函数的参数在执行 defer 语句时计算,而不是在实际函数调用完成时计算。

让我们通过一个例子来理解这一点。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func printA(a int) {
  6. fmt.Println("value of a in deferred function", a)
  7. }
  8. func main() {
  9. a := 5
  10. defer printA(a)
  11. a = 10
  12. fmt.Println("value of a before deferred function call", a)
  13. }

Run in playground

在上面的程序中,在 12 行 a 最初的值为 5。 在 13 行执行 defer 语句时,a 的值为5,因此这将是 printA 函数的延迟参数。 我们在 14 行将 a 的值更改为 10。 下一行输出 a 的值。 该程序输出,

  1. value of a before deferred function call 10
  2. value of a in deferred function 5

从上面的输出可以理解,尽管在执行延迟语句之后 a 的值变为 10,但实际的延迟函数调用 printA(a) 仍然输出 5

defer 的堆栈

当一个函数有多个延迟调用时,它们会被添加到堆栈中并以后进先出(LIFO)顺序执行。

我们将编写一个小程序,使用一堆 defer 来反向输出字符串。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. name := "Naveen"
  7. fmt.Printf("Original String: %s\n", string(name))
  8. fmt.Printf("Reversed String: ")
  9. for _, v := range []rune(name) {
  10. defer fmt.Printf("%c", v)
  11. }
  12. }

Run in playground

在上面的程序中,在第 11 行的 for range 循环中,迭代字符串,并在第 12 行调用 defer fmt.Printf(“%c”, v)。这些 defer 调用将被添加到堆栈中。

  1. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/137440/1598669161628-a9bf4f99-5cae-47dd-93f9-faac0bb30ac6.png#align=left&display=inline&height=281&margin=%5Bobject%20Object%5D&name=image.png&originHeight=281&originWidth=181&size=12075&status=done&style=none&width=181)

上图表示的是添加 defer 调用后的堆栈内容。)是一个后进先出的数据结构。最后被推到栈中的 defer 调用将被弹出并首先执行。在这种情况下,defer fmt.Printf(“%c”, ‘n’) 将被先执行,因此字符串将以相反的顺序打印。

  1. Original String: Naveen
  2. Reversed String: neevaN


defer 的实际用途

到目前为止我们看到的代码示例没有 defer 的实际用法。在本节中,我们将研究 defer的一些实际用途。

Defer 用于应该执行函数调用的地方,而不管代码流程如何。让我们用一个使用WaitGroup 的程序的例子来理解这一点。我们将首先编写程序而不使用 Defer,然后我们将修改它以使用 Defer 并理解延迟是多么有用。

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. type rect struct {
  7. length int
  8. width int
  9. }
  10. func (r rect) area(wg *sync.WaitGroup) {
  11. if r.length < 0 {
  12. fmt.Printf("rect %v's length should be greater than zero\n", r)
  13. wg.Done()
  14. return
  15. }
  16. if r.width < 0 {
  17. fmt.Printf("rect %v's width should be greater than zero\n", r)
  18. wg.Done()
  19. return
  20. }
  21. area := r.length * r.width
  22. fmt.Printf("rect %v's area %d\n", r, area)
  23. wg.Done()
  24. }
  25. func main() {
  26. var wg sync.WaitGroup
  27. r1 := rect{-67, 89}
  28. r2 := rect{5, -67}
  29. r3 := rect{8, 9}
  30. rects := []rect{r1, r2, r3}
  31. for _, v := range rects {
  32. wg.Add(1)
  33. go v.area(&wg)
  34. }
  35. wg.Wait()
  36. fmt.Println("All go routines finished executing")
  37. }

Run in playground

在上面的程序中,我们在第 8 行创建了一个rect 结构,在第 13 行创建了一个方法 area ,用于计算矩形的面积。 此方法检查矩形的长度和宽度是否小于零。 如果是这样,它会输出相应的消息,否则会输出矩形的区域。

main 函数创建了 3 个类型为 rect 的变量 r1, r2r3。 然后在 34 行将它们添加到rects 切片中。 然后使用 for range 循环迭代该切片,在 37 行并将 area 方法被调用成并发的 Goroutine。WaitGroup wg 用于确保主函数被阻塞,直到所有 Goroutines 完成执行。 WaitGroup 作为参数传递给 area 方法,在 16、21、26 行 area 方法调用wg.Done() 以通知 main 函数 Goroutine 完成其任务。 你可以注意到,这些调用恰好在 area 方法返回之前发生。 无论代码流采用何种路径,都应在方法返回之前调用 wg.Done(),因此可以通过单个 defer 有效地替换这些调用。

让我们使用 defer 重写上面的程序。

在下面的程序中,我们删除了上面程序中的 3 个 wg.Done() 调用,并将其替换为第 14 行中的单个 defer wg.Done() 调用。 这使代码更简单易懂。

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. )
  6. type rect struct {
  7. length int
  8. width int
  9. }
  10. func (r rect) area(wg *sync.WaitGroup) {
  11. defer wg.Done()
  12. if r.length < 0 {
  13. fmt.Printf("rect %v's length should be greater than zero\n", r)
  14. return
  15. }
  16. if r.width < 0 {
  17. fmt.Printf("rect %v's width should be greater than zero\n", r)
  18. return
  19. }
  20. area := r.length * r.width
  21. fmt.Printf("rect %v's area %d\n", r, area)
  22. }
  23. func main() {
  24. var wg sync.WaitGroup
  25. r1 := rect{-67, 89}
  26. r2 := rect{5, -67}
  27. r3 := rect{8, 9}
  28. rects := []rect{r1, r2, r3}
  29. for _, v := range rects {
  30. wg.Add(1)
  31. go v.area(&wg)
  32. }
  33. wg.Wait()
  34. fmt.Println("All go routines finished executing")
  35. }

Run in playground

该程序输出,

  1. rect {8 9}'s area 72
  2. rect {-67 89}'s length should be greater than zero
  3. rect {5 -67}'s width should be greater than zero
  4. All go routines finished executing

在上述程序中使用 defer 还有一个优点。假设我们使用新的 if 条件向 area 方法添加另一个返回路径。如果没有 defer 对 wg.Done()的调用,我们必须小心并确保在这个新的返回路径中调用 wg.Done()。但由于对 wg.Done() 的调用被推迟,我们不必担心为此方法添加新的返回路径。

原文链接

https://golangbot.com/defer/