[译] part 29: golang defer

https://juejin.im/post/5ca2199fe51d45247027d73f

什么是 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. }

Run in playgroud 以上是一个简单的程序,用于查找给定切片最大的数。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函数开始执行并打印上述输出的前两行。在它返回之前,defer函数完成执行,并打印Finished finding largest :)

defer一个方法

defer不仅限于函数。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

在上面的程序中,我们defer了一个方法的调用,其余的代码是不难懂的。该程序输出,

  1. Welcome John Smith

defer的参数作用域

defer的函数的参数是在执行defer语句时传入的,在实际函数调用的时候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 playgroud

在上面的程序中,第 11 行a被初始化为 5,defer语句实在第 12 行。adefer函数printA的参数。在第 13 行我们将a的值更改为 10。该程序的输出,

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

从上面的输出可以看到,尽管在执行defer语句之后a的值变为 10,但实际的defer函数调用printA(a)仍然打印 5。

多个defer函数的调用顺序

当一个函数有多个defer调用时,它们会被添加到栈中并以后进先出(LIFO)的顺序执行。 我们将编写一个小程序,使用一系列defer来反向打印字符串。

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

Run in playgroud

在上面的程序中,第 11 行开始的for range循环迭代字符串并调用defer fmt.Printf("%c", v)输出字符。这些defer`调用将被添加到栈中并以后进先出的顺序执行,因此字符串将以相反的顺序打印。该程序将输出,

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

defer的实际用法

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

defer用于应该执行函数调用的地方,而不管代码流程如何???。让我们用一个使用WaitGroup的例子来理解这一点。我们将首先编写程序而不使用defer,然后我们将修改它以使用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 行给rect结构加上了area方法用于计算矩形的面积。此方法检查矩形的长度和宽度是否小于 0。如果是这样,它会打印相应的消息,否则会打印矩形的面积。

main函数创建了 3 个类型为rect的变量r1r2r3,将它们添加到rects切片中。然后使用for range循环迭代该切片,并将area方法并发执行。 WaitGroup wg用于保证所有Goroutines执行完毕。WaitGroup作为参数传递给area方法,并在area方法中调用wg.Done,主要通知mainGoroutine已完成其工作。如果您仔细观察,可以看到这些调用恰好在area方法返回之前发生。无论代码采用哪个条件分支执行,都应在方法返回之前调用wg.Done,因此可以通过defer来解决这种场景。

来用defer重写上面的程序吧。

在下面的程序中,我们删除了上面程序中的 3 个wg.Done,并将其替换为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 palyground 输出,

  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方法添加另一个处理分支。如果没有deferwg.Done的调用,我们必须小心确保在这个新的处理分支中调用wg.Done。但由于对wg.Done的调用用了defer,我们再也不用担心这种情况了。相似的应用场景应该还有很多,比如打开文件的关闭等等。但是需要注意的是,大量的使用defer函数会导致程序运行效率变低。

ft_authoradmin  ft_create_time2019-08-03 16:40
 ft_update_time2019-08-03 16:40