「读书笔记」使用 defer 让函数更简洁、更健壮

168 阅读4分钟

函数与方法

22 使用 defer 让函数更简洁、更健壮

程序往往需要在函数中申请一些资源(如文件描述符和互斥锁),并在函数退出前释放或关闭这些资源。函数的实现需要确保这些资源在函数退出时被及时正确地释放,无论函数的执行流是按预期顺利进行还是出现错误提前退出。为此,开发人员需要对函数中的错误处理尤为关注,在错误处理时不能遗漏对资源的释放,尤其是有多个资源需要释放的时候。这大大增加了开发人员的心智负担。此外,当待释放资源个数较多时,代码逻辑将变得十分复杂,程序可读性、健壮性随之下降。但即便如此,如果函数实现中的某段代码逻辑抛出 panic,传统的错误处理机制依然没有办法捕获它并尝试从 panic 中恢复。因此 Go 引入了 defer。

defer 的运作离不开函数,这至少有两层含义:

  • 在 Go 中,只有在函数和方法内部才能使用 defer;
  • defer 关键字后面只能接函数或方法,这些函数被称为 deferred 函数。defer 将它们注册到其所在 goroutine 用于存放 deferred 函数的数据结构中,这些 deferred 函数将在执行 defer 的函数退出前按后进先出(LIFO)的顺序调度执行。

无论是执行到函数体尾部返回,还是在某个错误处理分支显式调用 return 返回。抑或出现 panic,已经存储到 deferred 函数栈中的函数都会被调度执行。因此,deferred 函数是一个在任何情况下都可以为函数进行收尾工作的好场合。

defer 优点:资源释放函数的 defer 注册动作紧邻着资源申请成功的动作,这样成对出现的惯例极大降低了遗漏资源释放的可能性,开发人员再也不用小心翼翼地在每个错误处理分支中检查是否遗漏了某个资源的释放动作。同时,代码的简化又意味着代码可读性的提高以及健壮性的增强。

defer 的常见用法:

  • 拦截 panic:按需要对 panic 进行处理,也可以尝试从 panic 中恢复。

    // $GOROOT/src/bytes/buffer.go
    func makeSlice(n int) []byte {
       // If the make fails, give a known error.
       defer func() {
          if recover() != nil {
             // 触发一个新 panic,但为新 panic 传一个新的 error 值
             panic(ErrTooLarge)
          }
       }()
       return make([]byte, n)
    }
    
  • 修改函数的具名返回值

    func foo(a, b int) (x, y int) {
       defer func() {
          x = x * 5
          y = y * 10
       }()
    
       x = a + 5
       y = b + 6
       return
    }
    
    func main() {
       x, y := foo(1, 2)
       fmt.Println("x=", x, "y=", y)
       // x= 30 y= 80
    }
    
  • 输出调试信息

    func trace(s string) string {
       fmt.Println("entering", s)
       return s
    }
    
    func un(s string) {
       fmt.Println("leaving:", s)
    }
    
    func a() {
       defer un(trace("a"))
       fmt.Println("in a")
    }
    
    func b() {
       defer un(trace("b"))
       fmt.Println("in b")
       a()
    }
    
    func main() {
       b()
       // entering b
       // in b
       // entering a
       // in a
       // leaving: a
       // leaving: b
    }
    
  • 还原变量旧值

    var x = []int{1}
    
    func foo() {
       old := x[:]
       defer func() { x = old }()
       x = append(x, 2)
       fmt.Println("foo:", x)
    }
    
    func main() {
       foo()
       fmt.Println("main:", x)
       // foo: [1 2]
       // main: [1]
    }
    

关于 defer 的几个关键问题:

  • 明确哪些函数可以作为 deferred 函数:对于自定义的函数或方法,defer 可以给予无条件的支持,但是对于有返回值的自定义函数或方法,返回值会在 deferred 函数被调度执行的时候被自动丢弃。对于内置函数,append、cap、len、make、new 等不可以直接作为 deferred 函数,而 close、copy、delete、print、recover 可以。

  • 把握好 defer 关键字后表达式的求值时机:defer 关键字后面的表达式是在将 deferred 函数注册到 deferred 函数栈的时候进行求值的。

    func foo1() {
       for i := 0; i <= 3; i++ {
          defer fmt.Println(i)
       }
    }
    
    func foo2() {
       for i := 0; i <= 3; i++ {
          defer func(n int) {
             fmt.Println(n)
          }(i)
       }
    }
    
    func foo3() {
       for i := 0; i <= 3; i++ {
          defer func() {
             fmt.Println(i)
          }()
       }
    }
    
    func main() {
       fmt.Println("foo1 result:")
       foo1() // 3 2 1 0
       fmt.Println("\nfoo2 result:")
       foo2() // 3 2 1 0
       fmt.Println("\nfoo3 result:")
       foo3() // 4 4 4 4
    }
    
    func foo1() {
       sl := []int{1, 2, 3}
       defer func(a []int) {
          fmt.Println(a)
       }(sl)
    
       sl = []int{3, 2, 1}
       _ = sl
    }
    
    func foo2() {
       sl := []int{1, 2, 3}
       defer func(p *[]int) {
          fmt.Println(*p)
       }(&sl)
    
       sl = []int{3, 2, 1}
       _ = sl
    }
    
    func main() {
       foo1() // [1 2 3]
       foo2() // [3 2 1]
    }
    
  • 知晓 defer 带来的性能损耗:在 Go1.14 版本中,defer 性能提升巨大,已经和不用 defer 的性能相差很小了。

往期回顾

关注我

掘金:XQGang

Github: XQ-Gang

参考

《Go 语言精进之路:从新手到高手的编程思想、方法和技巧》——白明