听说,99% 的 Go 程序员都被 Defer 坑过

开发 后端
直接把我珍藏多年的代码一把梭,凭借多年踩坑经历和写 BUG 经验,我要站着把这个坑迈过去。

[[429635]]

先声明:我被坑过。

之前写 Go 专栏时,写过一篇文章:Go 专栏|错误处理:defer,panic 和 recover。有小伙伴留言说:道理都懂,但还是不知道怎么用,而且还总出现莫名奇妙的问题。

出问题就对了,这个小东西坏的很,一不留神就出错。

所以,面对这种情况,我们今天就不讲道理了。直接把我珍藏多年的代码一把梭,凭借多年踩坑经历和写 BUG 经验,我要站着把这个坑迈过去。

一、

先来一个简单的例子热热身:

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.  
  6. func main() { 
  7.     defer func() { 
  8.         fmt.Println("first"
  9.     }() 
  10.  
  11.     defer func() { 
  12.         fmt.Println("second"
  13.     }() 
  14.  
  15.     fmt.Println("done"

输出:

  1. done 
  2. second 
  3. first 

这个比较简单,defer 语句的执行顺序是按调用 defer 语句的倒序执行。

二、

看看这段代码有什么问题?

  1. for _, filename := range filenames { 
  2.     f, err := os.Open(filename) 
  3.     if err != nil { 
  4.         return err 
  5.     } 
  6.     defer f.Close() 

这段代码其实很危险,很可能会用尽所有文件描述符。因为 defer 语句不到函数的最后一刻是不会执行的,也就是说文件始终得不到关闭。所以切记,一定不要在 for 循环中使用 defer 语句。

那怎么优化呢?可以将循环体单独写一个函数,这样每次循环的时候都会调用关闭函数。

如下:

  1. for _, filename := range filenames { 
  2.     if err := doFile(filename); err != nil { 
  3.         return err 
  4.     } 
  5.  
  6. func doFile(filename string) error { 
  7.     f, err := os.Open(filename) 
  8.     if err != nil { 
  9.         return err 
  10.     } 
  11.     defer f.Close() 

三、

看看这三个函数的输出结果是什么?

  1. package main 
  2.  
  3. import ( 
  4.     "fmt" 
  5.  
  6. func a() (r int) { 
  7.     defer func() { 
  8.         r++ 
  9.     }() 
  10.     return 0 
  11.  
  12. func b() (r int) { 
  13.     t := 5 
  14.     defer func() { 
  15.         t = t + 5 
  16.     }() 
  17.     return t 
  18.  
  19. func c() (r int) { 
  20.     defer func(r int) { 
  21.         r = r + 5 
  22.     }(r) 
  23.     return 1 
  24.  
  25. func main() { 
  26.     fmt.Println("a = ", a()) 
  27.     fmt.Println("b = ", b()) 
  28.     fmt.Println("c = ", c()) 

公布答案:

  1. a =  1 
  2. b =  5 
  3. c =  1 

你答对了吗?

说实话刚开始看到这个结果时,我是相当费解,完全不知道怎么回事。

但可以看到,这三个函数都有一个共同特点,它们都有一个命名返回值,并且都在函数中引用了这个返回值。

引用的方式分两种:分别是闭包和函数参数。

先看 a() 函数:

闭包通过 r++ 修改了外部变量,返回值变成了 1。

相当于:

  1. func aa() (r int) { 
  2.     r = 0 
  3.     // 在 return 之前,执行 defer 函数 
  4.     func() { 
  5.         r++ 
  6.     }() 
  7.     return 

再看 b() 函数:

闭包内修改的只是局部变量 t,而外部变量 t 不受影响,所以还是返回 5。

相当于:

  1. func bb() (r int) { 
  2.     t := 5 
  3.     // 赋值 
  4.     r = t 
  5.     // 在 return 之前,执行 defer 函数 
  6.     // defer 函数没有对返回值 r 进行修改,只是修改了变量 t 
  7.     func() { 
  8.         t = t + 5 
  9.     }() 
  10.     return 

最后是 c 函数:

参数传递是值拷贝,实参不受影响,所以还是返回 1。

相当于:

  1. func cc() (r int) { 
  2.     // 赋值 
  3.     r = 1 
  4.     // 这里修改的 r 是函数形参的值 
  5.     // 值拷贝,不影响实参值 
  6.     func(r int) { 
  7.         r = r + 5 
  8.     }(r) 
  9.     return 

那么,为了避免写出这么令人意外的代码,最好在定义函数时就不要使用命名返回值。或者如果使用了,就不要在 defer 中引用。

再看下面两个例子:

  1. func d() int { 
  2.     r := 0 
  3.     defer func() { 
  4.         r++ 
  5.     }() 
  6.     return r 
  7.  
  8. func e() int { 
  9.     r := 0 
  10.     defer func(i int) { 
  11.         i++ 
  12.     }(r) 
  13.     return 0 
  14. d =  0 
  15. e =  0 

返回值符合预期,再也不用绞尽脑汁猜了。

四、

defer 表达式的函数如果在 panic 后面,则这个函数无法被执行。

  1. func main() { 
  2.     panic("a"
  3.     defer func() { 
  4.         fmt.Println("b"
  5.     }() 

输出如下,b 没有打印出来。

  1. panic: a 
  2.  
  3. goroutine 1 [running]: 
  4. main.main() 
  5.     xxx.go:87 +0x4ce 
  6. exit status 2 

而如果 defer 在前,则可以执行。

  1. func main() { 
  2.     defer func() { 
  3.         fmt.Println("b"
  4.     }() 
  5.     panic("a"

输出:

  1. panic: a 
  2.  
  3. goroutine 1 [running]: 
  4. main.main() 
  5.     xxx.go:90 +0x4e7 
  6. exit status 2 

五、

看看下面这段代码的执行顺序:

  1. func G() { 
  2.     defer func() { 
  3.         fmt.Println("c"
  4.     }() 
  5.  
  6.     F() 
  7.     fmt.Println("继续执行"
  8.  
  9. func F() { 
  10.     defer func() { 
  11.         if err := recover(); err != nil { 
  12.             fmt.Println("捕获异常:", err) 
  13.         } 
  14.         fmt.Println("b"
  15.     }() 
  16.     panic("a"
  17.  
  18. func main() { 
  19.     G() 

顺序如下:

  1. 调用 G() 函数;
  2. 调用 F() 函数;
  3. F() 中遇到 panic,立刻终止,不执行 panic 之后的代码;
  4. 执行 F() 中 defer 函数,遇到 recover 捕获错误,继续执行 defer 中代码,然后返回;
  5. 执行 G() 函数后续代码,最后执行 G() 中 defer 函数。

输出:

  1. 捕获异常: a 
  2. 继续执行 

五、

看看下面这段代码的执行顺序:

  1. func G() { 
  2.     defer func() { 
  3.         if err := recover(); err != nil { 
  4.             fmt.Println("捕获异常:", err) 
  5.         } 
  6.         fmt.Println("c"
  7.     }() 
  8.  
  9.     F() 
  10.     fmt.Println("继续执行"
  11.  
  12. func F() { 
  13.     defer func() { 
  14.         fmt.Println("b"
  15.     }() 
  16.     panic("a"
  17.  
  18. func main() { 
  19.     G() 

顺序如下:

  1. 调用 G() 函数;
  2. 调用 F() 函数;
  3. F() 中遇到 panic,立刻终止,不执行 panic 之后的代码;
  4. 执行 F() 中 defer 函数,由于没有 recover,则将 panic 抛到 G() 中;
  5. G() 收到 panic 则不会执行后续代码,直接执行 defer 函数;
  6. defer 中捕获 F() 抛出的异常 a,然后继续执行,最后退出。

输出:

  1. 捕获异常: a 

六、

看看下面这段代码的执行顺序:

  1. func G() { 
  2.     defer func() { 
  3.         fmt.Println("c"
  4.     }() 
  5.  
  6.     F() 
  7.     fmt.Println("继续执行"
  8.  
  9. func F() { 
  10.     defer func() { 
  11.         fmt.Println("b"
  12.     }() 
  13.     panic("a"
  14.  
  15. func main() { 
  16.     G() 

顺序如下:

  1. 调用 G() 函数;
  2. 调用 F() 函数;
  3. F() 中遇到 panic,立刻终止,不执行 panic 之后的代码;
  4. 执行 F() 中 defer 函数,由于没有 recover,则将 panic 抛到 G() 中;
  5. G() 收到 panic 则不会执行后续代码,直接执行 defer 函数;
  6. 由于没有 recover,直接抛出 F() 抛过来的异常 a,然后退出。

输出:

  1. panic: a 
  2.  
  3. goroutine 1 [running]: 
  4. main.F() 
  5.     xxx.go:90 +0x5b 
  6. main.G() 
  7.     xxx.go:82 +0x48 
  8. main.main() 
  9.     xxx.go:107 +0x4a5 
  10. exit status 2 

七、

看看下面这段代码的执行顺序:

  1. func G() { 
  2.     defer func() { 
  3.         // goroutine 外进行 recover 
  4.         if err := recover(); err != nil { 
  5.             fmt.Println("捕获异常:", err) 
  6.         } 
  7.         fmt.Println("c"
  8.     }() 
  9.  
  10.     // 创建 goroutine 调用 F 函数 
  11.     go F() 
  12.     time.Sleep(time.Second
  13.  
  14. func F() { 
  15.     defer func() { 
  16.         fmt.Println("b"
  17.     }() 
  18.     // goroutine 内部抛出panic 
  19.     panic("a"
  20.  
  21. func main() { 
  22.     G() 

顺序如下:

  1. 调用 G() 函数;
  2. 通过 goroutine 调用 F() 函数;
  3. F() 中遇到 panic,立刻终止,不执行 panic 之后的代码;
  4. 执行 F() 中 defer 函数,由于没有 recover,则将 panic 抛到 G() 中;
  5. 由于 goroutine 内部没有进行 recover,则 goroutine 外部函数,也就是 G() 函数是没办法捕获的,程序直接崩溃退出。

输出:

  1. panic: a 
  2.  
  3. goroutine 6 [running]: 
  4. main.F() 
  5.     xxx.go:96 +0x5b 
  6. created by main.G 
  7.     xxx.go:87 +0x57 
  8. exit status 2 

八、

最后再说一个 recover 的返回值问题:

  1. defer func() { 
  2.     if err := recover(); err != nil { 
  3.         fmt.Println("捕获异常:", err.Error()) 
  4.     } 
  5. }() 
  6. panic("a"

recover 返回的是 interface {} 类型,而不是 error 类型,所以这样使用的话会报错:

  1. err.Error undefined (type interface {} is interface with no methods) 

可以这样来转换一下:

  1. defer func() { 
  2.     if err := recover(); err != nil { 
  3.         fmt.Println("捕获异常:", fmt.Errorf("%v", err).Error()) 
  4.     } 
  5. }() 
  6. panic("a"

或者直接打印结果:

  1. defer func() { 
  2.     if err := recover(); err != nil { 
  3.         fmt.Println("捕获异常:", err) 
  4.     } 
  5. }() 
  6. panic("a"

输出:

  1. 捕获异常: a 

以上就是本文的全部内容,其实写过其他的语言的同学都知道,关闭文件句柄,释放锁等操作是很容易忘的。而 Go 语言通过 defer 很好地解决了这个问题,但在使用过程中还是要小心。

本文总结了一些容踩坑的点,希望能够帮助大家少写 BUG,如果大家觉得有用的话,欢迎点赞和转发。

文章中的脑图和源码都上传到了 GitHub,有需要的同学可自行下载。

源码地址:

https://github.com/yongxinz/gopher/tree/main/sc

本文转载自微信公众号「AlwaysBeta」,可以通过以下二维码关注。转载本文请联系AlwaysBeta公众号。

 

责任编辑:武晓燕 来源: AlwaysBeta
相关推荐

2022-07-15 08:20:54

Java基础知识

2020-10-09 07:54:43

PythonJava爬虫

2019-10-25 22:17:25

开发者技能工具

2020-08-05 07:53:53

程序员网站技术

2018-02-06 08:36:02

简历程序员面试

2015-05-15 10:09:09

程序员

2024-03-26 00:48:38

2020-09-14 08:47:46

缓存程序员存储

2015-09-16 09:57:41

swoolePHP程序员

2024-04-01 08:05:27

Go开发Java

2023-11-13 08:34:01

Java编程习惯

2024-03-13 13:10:48

JavaInteger缓存

2014-08-13 11:11:58

程序员

2020-10-28 09:43:40

前端开发Vue

2022-09-25 21:58:27

程序员

2013-08-20 09:33:59

程序员

2018-10-11 10:41:12

Go 开发技术

2018-09-28 07:00:03

编程语言Go语言

2015-06-02 11:01:18

JavaGo

2019-11-06 10:49:29

谷歌Android开发者
点赞
收藏

51CTO技术栈公众号