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

93 阅读2分钟

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

运作机制
  • 只有函数和方法内部才能使用defer
  • defer关键字后面只能接函数或方法,这些函数被称为deferred函数。defer将它们注册到其所在goroutine用于存放defered函数的栈数据结构中,这些defered函数将在执行defer的函数推出现被按 **后进先出 **的顺序调度

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

常见用法
  1. 拦截panic

    可以按需对拦截到的panic进行处理,可以尝试从panic恢复

    // 捕获一个panic,同时触发新的panic
    func makeSlice(n int) []byte {
        // If the make fails, give a known error.
        defer func() {
            if recover() != nil {
                panic(ErrTooLarge) // 触发一个新panic
            }
        }()
        return make([]byte, n)
    }
    
    // 拦截panic并恢复了程序的运行
    func bar() {
        fmt.Println("raise a panic")
        panic(-1)
    }
    
    func foo() {
        defer func() {
            if e := recover(); e != nil {
                fmt.Println("recovered from a panic")
            }
        }()
        bar()
    }
    
    func main() {
        foo()
        fmt.Println("main exit normally")
    }
    
  2. 修改函数的具名返回值

  3. 输出调试信息

    deferred函数被注册及调度执行的时间点使得它十分适合用来输出一些调试信息。比如,Go标准库中net包中的hostLookupOrder方法就使用deferred函数在特定日志级别下输出一些日志以便于程序调试和跟踪。

  4. 还原变量旧值

关于defer的关键问题
  1. 明确哪些函数可以作为defered函数

    对于自定义的函数或方法,defer可以给予无条件的支持,但是对于有返回值的自定义函数,defer会丢弃所有返回值。

    对于内置函数如:append cap len complex image make new panic println real不可以直接作为defered的函数,close copy delete print recover可以,对于不能直接使用的内置函数,可以通过匿名函数包装来使用。

  2. 把握好defer关键字后表达式的求值时机

  3. 知晓defer带来的性能损耗

小结

在多数情况下,我们的程序对性能不太敏感,建议尽量使用defer。defer让资源释放变得优雅且不易出错,简化了函数实现逻辑,提高了代码可读性,让函数实现变得更加健壮。