[关闭]
@phper 2018-03-13T10:07:03.000000Z 字数 4016 阅读 667

29.推迟(defer)

Golang

原文:https://golangbot.com/defer/


欢迎访问Golang 系列教程中的第29章。

什么是延迟(defer)?

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

例如

  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. }

在playground上运行

上面是一个简单的程序用来找出一个切片数组中的最大值。largest函数采用int切片作为参数, 并打印输入切片的最大值。largest函数的第一行包含语句defer finished()。这意味着finished()函数将在largest函数返回之前调用 (也就是largest函数全部执行完成后,再去执行finished())。运行此程序, 您可以看到以下输出打印。

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

largest函数开始执行并打印上面的前两行。在它返回之前, 我们的延迟函数finished执行并打印文本Finished finding largest

延迟方法 (defer method)

Defer 并不仅仅限于函数(function)。延迟方法(method)调用也是合法的。我们来编写一个小程序来测试它。

  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. }

在playground上运行

在上面的程序中, 我们推迟了22行的方法调用。其余的程序是自我解释。这个程序输出:

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. }

在playground上运行

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

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

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

延迟堆栈

当函数有多个延迟调用时, 它们将添加到堆栈上, 并在最后一个 "先出" (LIFO) 顺序中执行。

我们将编写一个小程序, 打印一个字符串反向使用一堆延迟。

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

在playground上运行

在上面的程序中,for range循环位于11行, 循环访问该字符串并调用defer fmt.Printf("%c", v) 。这些延迟调用将被添加到堆栈中, 最后以先出顺序执行, 因此字符串将以相反的顺序打印。这个程序将输出,

Orignal String: Naveen  
Reversed String: neevaN  

延迟的实际使用

到目前为止, 我们看到的代码示例不显示延迟的实际使用。在本节中, 我们将研究Defer的一些实际用途。

延迟用于在执行函数调用的地方, 而不管代码流如何。让我们以使用WaitGroup的程序的示例来理解这一点。我们将先编写程序而不使用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. }

在playground上运行

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

main函数创建了3个类型为rect的变量r1r2r3。然后将它们添加到第34行中的rects切片中。然后使用for range此切片进行迭代, 并将 range 方法称为37行中的并发area Goroutine 。WaitGroup wg用于确保主函数被阻止, 直到所有 Goroutines 完成执行为止。此 WaitGroup 作为参数传递给区域方法, 并且区域方法调用wg.Done()在16、21和26行中, 通知主要功能 Goroutine 完成其工作。如果您注意到, 则可以看到这些调用恰好在area方法返回之前, 应调用 wg.Done(), 而不考虑代码流所采用的路径, 因此这些调用可以通过一个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. }

在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

在上面的程序中使用延迟还有一个优点。让我们说, 我们使用新的if条件添加另一个返回路径到area方法。如果调用wg.Done()未延迟, 我们必须小心, 并确保我们调用wg.Done()在此新的返回路径中。但自调用wg.Done()是 defered, 我们不必担心向此方法添加新的返回路径。

这使我们到本教程的末尾。祝你今天开心。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注