延迟调用

关键字 defer

上一章中,代码里出现了defer关键字

defer特性:

  1. 关键字 defer 用于注册延迟调用。
  2. 这些调用直到 return 前才被执行。因此,可以用来做资源清理。
  3. 多个defer语句,按先进后出的方式执行。
  4. defer语句中的变量,在defer声明时就决定了。

defer用途:

  1. 关闭文件句柄
  2. 锁资源释放
  3. 数据库连接释放
  4. 确保指定代码的执行

使用场景demo:

func test() error {
    f, err := os.Create("test.txt")
    if err != nil { return err }

    defer f.Close() //延迟调用,关闭资源

    f.WriteString("Hello, World!")        
    return nil
}

延迟执行次序

多个 defer 注册,按 FILO 次序执行(defer栈,先进后出)。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

func main() {
    defer fmt.Print("a")
    defer fmt.Print("b")
    defer fmt.Print("c")
}
/*
    输出cba
*/

延迟调用中变参与闭包值的问题

延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。(此处在上一章提到)

package main

import "fmt"

func test() {
    x, y := 10, 20

    defer func(i int) {
        println("defer:", i, y) // y 闭包引用指针
    }(x) // x 被复制

    x += 10
    y += 100
    fmt.Println("x =", x, "y =", y)
}

func main() {
    test()
}
/*
    输出:
    x = 20 y = 120
    defer: 10 120
*/

因此就会出现下面的现象

package main

import "fmt"

func main() {
    var whatever [5]struct{}
    for i := range whatever {
        defer func() { fmt.Print(i) }()//执行时i已经是4了,所以输出的都是4
    }
}
/*
    输出:44444
*/

不要滥用defer

滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。(defer涉及线程同步问题,降低运行效率)

错误处理

错误的抛出与捕获

没有结构化异常,使用 panic 抛出错误,recover 捕获错误。

func test() {
    defer func() {//把此处的defer理解为java中的finally
        if err := recover(); err != nil {//捕获错误,并判断错误是否为空
            str := err.(string) // 将 interface{} 转型为具体类型。
           println(str)
        }
    }()
    panic("panic error!")
}

由于 panicrecover 参数类型为 interface{},因此可抛出任何类型对象。

//panic 和 recover的声明
func panic(v interface{})
func recover() interface{}

recover() 中需要注意的点

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

func test() {
    defer func() {
        fmt.Println(recover())
    }()

    defer func() {
        panic("defer panic")
    }()

    panic("test panic")
}

func main() {
    test() //输出:defer panic
}

捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递(类似java)

//因此这样的调用是无效的
defer recover() //defer should not call recover() directly 
defer fmt.Println(recover()) //同上
defer func(){ // 闭包层级过多?存疑
    func() {
        recover()
    }
}

//使用延迟匿名函数是有效的。
defer func(){
    recover()
}

//将问题处理函数提取也是有效的
func except() {
    recover()
}

func test() {
    defer except()
    panic("test panic")
}

保护代码片段

如果需要保护代码片段,可将代码块重构成匿名函数,如此可确保后续代码被执行。

func test(){
    //code
    func(){
        defer func(){
            if recover()!=nil{
                //处理方式
            }
        }
        //可能会产生panic的代码
    }
    //后续代码能顺利被执行
}

Go实现类似 try catch 的异常处理

package main

import "fmt"

func Try(fun func(), handler func(interface{})) {
    defer func() {
        if err := recover(); err != nil {
            handler(err)
        }
    }()
    fun()
}

func main() {
    Try(func() {
        panic("test panic")
    }, func(err interface{}) {
        fmt.Println(err)
    })
}

panic 与 error

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。

//error的接口
type error interface {
    Error() string
}

似乎看起来errorpanic功能上有些重复,多余了

如何区别使用 panic 和 error 两种方式?

惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。

标准库 errors.Newfmt.Errorf 函数用于创建实现 error 接口的错误对象。

func New(text string) error
func Errorf(format string, a ...interface{}) error

通过判断错误对象实例来确定具体错误类型。

package main

import (
    "errors"
    "fmt"
)

var ErrDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
    if y == 0 {
        return 0, ErrDivByZero
    }

    return x / y, nil
}

func main() {
    defer func(){
        if eil:=recover();eil!=nil{
            //处理
        } 
    }()
    switch z, err := div(10, 0); err {
    case nil:
        fmt.Println(z)
    case ErrDivByZero:
        panic(err)//将error包装为一个panic,方便统一处理
    }
}

参考

Go 边看边练

golang·看云 延迟调用

gobyexample - panic


此 生 无 悔 恋 真 白 ,来 世 愿 入 樱 花 庄 。