Go面试必考题目之defer篇

https://gocn.vip/article/1746

来源:微信公众号《Go后端干货》

各种Go,后端技术,面试题分享,欢迎关注

下面程序分别输出什么?

  1. func f1() {
  2. for i := 0; i < 5; i++ {
  3. defer fmt.Println(i)
  4. }
  5. }
  6. func f2() {
  7. for i := 0; i < 5; i++ {
  8. defer func() {
  9. fmt.Println(i)
  10. }()
  11. }
  12. }
  13. func f3() {
  14. for i := 0; i < 5; i++ {
  15. defer func(n int) {
  16. fmt.Println(n)
  17. }(i)
  18. }
  19. }
  20. func f4() int {
  21. t := 5
  22. defer func() {
  23. t++
  24. }()
  25. return t
  26. }
  27. func f5() (r int) {
  28. defer func() {
  29. r++
  30. }()
  31. return 0
  32. }
  33. func f6() (r int) {
  34. t := 5
  35. defer func() {
  36. t = t + 5
  37. }()
  38. return t
  39. }
  40. func f7() (r int) {
  41. defer func(r int) {
  42. r = r + 5
  43. }(r)
  44. return 1
  45. }

同样的,我们先不急着作答,我们先来看看defer的讲解。

defer是什么及用途

defer是什么?

  1. defer 就像它的字面意思一样,就是延迟执行的意思,但是需要注意的是 defer 只能作用于函数,像变量赋值defer i = 10这种编译是会报错的。

defer函数的执行顺序

  1. defer 的函数会放入一个栈中,所以是先进后出的执行顺序,而被 defer 的函数在 return 之后执行。

清理释放资源

  1. 当我们打开一个文件时,用完之后我们需要 close 这个文件,否则会导致文件描述符泄露,不用 defer 的代码一般是这样写的:
  1. func CopyFile(dstName, srcName string) (written int64, err error) {
  2. src, err := os.Open(srcName)
  3. if err != nil {
  4. return
  5. }
  6. dst, err := os.Create(dstName)
  7. if err != nil {
  8. return
  9. }
  10. written, err = io.Copy(dst, src)
  11. dst.Close()
  12. src.Close()
  13. return
  14. }
  1. 上面代码文件都close了,但这里有一个问题:如果os.Open(srcName)成功了,然后在os.Create(dstName)发错误了,这时的 src 却没有 close ,这样就会导致src的文件描述符泄露了。
  2. 修复这个问题也很简单,我们在os.Create(dstName)发生错误的时候,将src close掉就行了。但是问题又来了如果是多个文件同时打开,那这段代码将会非常的臃肿,而且会很容易的漏掉一些文件的 close
  3. 使用defer可以完美的解决这个问题。
  1. func CopyFile(dstName, srcName string) (written int64, err error) {
  2. src, err := os.Open(srcName)
  3. if err != nil {
  4. return
  5. }
  6. defer src.Close()
  7. dst, err := os.Create(dstName)
  8. if err != nil {
  9. return
  10. }
  11. defer dst.Close()
  12. return io.Copy(dst, src)
  13. }
  1. 上面代码中,只需在文件操作成功的时候调用 defer close 就可以了,而且文件的 open close 放在一起也不容易漏掉。

执行recover

  1. defer 的函数在 return 之后执行,这个时机点正好可以捕获函数抛出的 panic,因而defer 的另一个重要用途就是执行 recover ,而 recover 也只有在 defer 中才会起作用。
  1. func test() {
  2. defer func() {
  3. if ok := recover(); ok != nil {
  4. fmt.Println("recover")
  5. }
  6. }()
  7. panic("error")
  8. }
  1. 另外需要注意的是,recover 要放在 panic 点的前面,一般放在函数的起始的位置就可以了。

defer与return的关系

  1. Go 的函数返回值是通过堆栈返回的,这也是实现了多返回值的方法。看以下代码:
  1. // foo.go
  2. package main
  3. func foo() (int, int) {
  4. i := 1
  5. j := 2
  6. return i, j
  7. }
  8. func main() {
  9. foo()
  10. }

生成的汇编代码如下:

  1. $ go build -gcflags '-l' -o foo foo.go
  2. $ go tool objdump -s "main\.foo" foo
  3. TEXT main.foo(SB) /Users/kltao/code/go/src/example/foo.go
  4. bar.go:6 0x104ea70 48c744240801000000 MOVQ $0x1, 0x8(SP)
  5. bar.go:6 0x104ea79 48c744241002000000 MOVQ $0x2, 0x10(SP)
  6. bar.go:6 0x104ea82 c3 RET

也就是说 return 语句不是原子操作,它被拆成了两步

  1. rval = xxx // 返回值赋值给rval
  2. ret // 函数返回
  3. defer 语句就是在这两条语句之间执行,也就是
  1. rval = xxx // 返回值赋值给rval
  2. defer_func // 执行defer函数
  3. ret // 函数返回
  1. 上面的题目中,还涉及到另外一个知识点,那就是闭包。
  2. 简单来说,Go 语言中的闭包就是在函数内引用函数体之外的数据,这样就会产生一种结果,虽然数据定义是在函数外,但是在函数内部操作数据也会对数据产生影响。看下面的例子:
  1. func foo() {
  2. i := 1
  3. func() {
  4. i++
  5. }
  6. fmt.Println(i) // 输出2
  7. }
  1. 上面的 i 就是一个闭包引用,当匿名函数执行时,i 也会被修改。

题目解析

  1. 下面统一将rval称为函数最终return的变量值
  1. func f1() {
  2. for i := 0; i < 5; i++ {
  3. defer fmt.Println(i)
  4. }
  5. }
  6. // 因为defer的调用是先进后出的顺序
  7. // 所以输出:5, 4, 3, 2, 1
  8. func f2() {
  9. for i := 0; i < 5; i++ {
  10. defer func() {
  11. fmt.Println(i)
  12. }()
  13. }
  14. }
  15. // 上面说到,i是一个闭包引用
  16. // 所以当执行defer时,i已经是5了
  17. // 所以输出:5,5,5,5,5
  18. func f3() {
  19. for i := 0; i < 5; i++ {
  20. defer func(n int) {
  21. fmt.Println(n)
  22. }(i)
  23. }
  24. }
  25. // Go的函数参数是值拷贝,所以这是普通的函数传值
  26. // 所以输出:5,4,3,2,1
  27. func f4() int {
  28. t := 5
  29. defer func() {
  30. t++
  31. }()
  32. return t
  33. }
  34. // 注意:f4函数的返回值是没有声明变量的
  35. // 所以t虽然是闭包引用,但返回值rval不是闭包引用
  36. // 可以拆解为
  37. // rval = t
  38. // t++
  39. // return rval
  40. // 所以输出是5
  41. func f5() (r int) {
  42. defer func() {
  43. r++
  44. }()
  45. return 0
  46. }
  47. // 注意:f5函数的返回值是有声明变量的
  48. // 所以返回值r是闭包引用
  49. // 可以拆解为
  50. // r = 0
  51. // rval = r
  52. // r++
  53. // return rval
  54. // 所以输出:1
  55. func f6() (r int) {
  56. t := 5
  57. defer func() {
  58. t = t + 5
  59. }()
  60. return t
  61. }
  62. // 这里t虽然是闭包引用,但返回值r不是闭包引用
  63. // 可以拆解为
  64. // r = t
  65. // rval = r
  66. // t = t + 5
  67. // return rval
  68. // 所以输出:5
  69. func f7() (r int) {
  70. defer func(r int) {
  71. r = r + 5
  72. }(r)
  73. return 1
  74. }
  75. // 因为匿名函数的参数也是r,所以相当于是
  76. // 匿名函数的参数r = r + 5,不影响外部
  77. // 所以输出:1
  78. 做这种defer的题,需要注意返回值是否为闭包引用。

总结

  1. 谨记defer和return执行的顺序
  2. 注意返回值是否为闭包引用

参考文献

1.《理解Go语言defer关键字的原理》https://draveness.me/golang-defer

2.《理解Go defer》https://sanyuesha.com/2017/07/23/go-defer/

3.《深入理解Go语言defer》https://mp.weixin.qq.com/s/e2t3CMUqtIcEq-OhbWy5Hw

2019-05-13

ft_authoradmin  ft_create_time2019-05-27 12:17
 ft_update_time2019-05-27 12:17