search.png
关于我
menu.png
go 异常处理

panic-recover机制

go中不像其它语言有try catch语法,同样也不推荐捕获异常而是推荐采用将异常返回的方式。但是对于一些会导致程序崩溃的严重异常,直接退出也会比层层返回方便的多。因此go中实现了panic-recover机制,专门用于对这些异常进行抛出和恢复。
在语义上,try catch更像尝试性的去做一些事,在这期间如果发生错误,则如何如何。而panic则是告诉你程序奔溃了,发生了特别严重的故障,到了难以恢复的程度。recover则是看看能不能救回来,如果不是极其致命,那么就继续服务吧,打印一下错误信息,返回一个500,应用继续运行。

基础语法

  1. recover 必须要放在defer中延迟执行
  2. recover 必须要是defer闭包或者调用函数下的第一层调用

下例中recover 1理论上是最后执行的。但是recover 2,3,4都无法捕获panic。所以结果是打印recover 1。

func testDefer1() {

    defer func() {
        // recover 必须要在defer闭包(函数)下的第一层调用
        err := recover()
        if err != nil {
            fmt.Println("recover 1", err)
        }
    }()

    defer func() {
        func() {
            err := recover()
            if err != nil {
                fmt.Println("recover 2", err)
            }
        }()
    }()

    defer recover()

    err := recover()
    if err != nil {
        fmt.Println("recover 3", err)
    }

    panic("1233")

    err = recover()
    if err != nil {
        fmt.Println("recover 4", err)
    }

}

  1. defer 中可以陆续再panic,但是recover接收的是最后一个:

下例中打印的是233:

// defer 中panic,recover接收的是最后一个
func testError2() {

    defer func() {
        err := recover()
        if err != nil {
            fmt.Println(err)
        }
    }()

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

    panic("err")
}

  1. recover一次后,后续recover将不再会被触发,结果是panic err, no err
// 使用函数来接收recover,这应该是更值得推荐的写法
func clearPanic() {
    if err := recover(); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println("no err")
    }
}

func testError3() {
    // recover一次后,后续recover将不再会被触发
    defer clearPanic()
    defer clearPanic()

    panic("panic err")
}

  1. 可以利用recover 来模拟try catch

func try(code func(), catch func(interface{})) {
    defer func() {
        if err := recover();err != nil {
            catch(err)
        }
    }()
    code()
}

// 利用defer 模拟try catch 结构
func testError4() {
    try(func() {
        panic("err")
    }, func(err interface{}) {
        fmt.Println("catch", err)
    })
}

  1. 使用panic-recover 无疑是会增加性能损耗的,以下对比测试了和直接返回error的性能差距:

func panicError() {
    panic("this is panic")
}

func returnError() error {
    return errors.New("this is error")
}

func recoverPanic() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    panicError()
}

func checkError() {
    if err := returnError(); err != nil {
        fmt.Println(err)
    }
}

func testError5() {
    const times = 100_0000
    t1 := time.Now()
    for i := 0; i < times; i++ {
        recoverPanic()
    }
    du1 := time.Since(t1)


    t2 := time.Now()
    for i := 0; i < times; i++ {
        checkError()
    }
    du2 := time.Since(t2)
    
    fmt.Println("panic recover use time:", du1)
    fmt.Println("check error use time:", du2)
}

运行耗时,此处使用的是go 1.16,在go 1.14大幅度优化了defer的性能,因此panic-recover的运行速度并不会慢太多,在100万次的运行中,性能差距0.1945303s:

panic recover use time: 2.0697743s
check error use time: 1.875244s
  1. 自定义异常,go的异常其实是个接口,只要实现了以下方法,就可以当作一个异常:
type error interface {
    Error() string
}

go 的errors.New可以用来新建异常:

var MomoError = errors.New("this is error")

func welcomeUnlessMomo(name string) error {
    if name == "momo" {
        return MomoError
    } else if name == "sb" {
        msg := "no welcome sb"
        fmt.Println(msg)
        return errors.New(msg)
    } else {
        fmt.Println("welcome", name)
    }
    return nil
}

func testError6(name string) {
    if err := welcomeUnlessMomo(name); err != nil && err == MomoError {
        fmt.Println("hello", "momo")
    }
}

输出结果:

welcome 123
hello momo
no welcome sb

亦可以实现Error方法来自定义异常:


var MomoError = errors.New("this is error")

type SbError struct {
    name string
}

func (sb *SbError) Error() string {
    return "sb: " + sb.name
}

func welcomeUnlessMomo(name string) error {
    if name == "momo" {
        return MomoError
    } else if name == "sb" {
        msg := "no welcome sb"
        fmt.Println(msg)
        return errors.New(msg)
    } else if name == "fool" || name == "stupid" {
        msg := "find a sb:" + name
        fmt.Println(msg)
        return &SbError{name}
    } else {
        fmt.Println("welcome", name)
    }
    return nil
}


func testError7() {
    if err := welcomeUnlessMomo("fool"); err != nil {
        if _,isSb := err.(*SbError); isSb {
            fmt.Println("sb error")
        }
    }
}

  1. errors的错误支持包装和解包:
func testError8() {
    // unwrap
    err1 := errors.New("1")
    err2 := fmt.Errorf("2:%w", err1)
    err3 := fmt.Errorf("3:%w", err2)
    err4 := fmt.Errorf("4:%w", err3)

    fmt.Println(err4)
    fmt.Println(errors.Unwrap(err4))
    fmt.Println(errors.Unwrap(errors.Unwrap(err4)))
    fmt.Println(errors.Unwrap(errors.Unwrap(errors.Unwrap(err4))))
    fmt.Println(errors.Unwrap(errors.Unwrap(errors.Unwrap(errors.Unwrap(err4)))))
}

打印结果:

4:3:2:1
3:2:1
2:1
1
<nil>

  1. Is和 == 似乎区别不大,结果都是一致的,true false true false。异常并不是字符串,比较时不是值比较的。
func testError9() {
    err := MomoError
    err2 := errors.New("this is error")
    fmt.Println(errors.Is(err, MomoError))
    fmt.Println(errors.Is(err2, MomoError))
    fmt.Println(err == MomoError)
    fmt.Println(err2 == MomoError)
}

  1. 当error一层包一层时,怎么确定error具有某种类型?errors.As就是用来判断这个的:
func testError10() {
    err := MomoError
    err2 := fmt.Errorf("wrap:%w", err)
    fmt.Println(errors.As(err2, &err))
}

打印结果是true。

最后

go panic-recover不好吗?很多人不推荐使用。这里说一下我的看法。

于我而言,go的返回error确实是比较舒适的,因为这样可以满足我的强迫症,细粒度的判断并解决每一个错误,相比panic的集中recover,在异常发生之后就决定要怎么做,我觉得会更恰当。panic是不可见的,而error返回值是总可见的。如果你调用的函数内部发生了panic,但是其内部并没有recover,而是抛到了外层,而你又没有去recover,那么这个panic就会导致你的代码运行失败,终止退出。我想,这是不推荐使用panic的最主要的原因之一。

但是,return error如果滥用也会造成像js语言promise.then那样的嵌套地狱。panic提供了一个直达上游的方法,这在一些需要进行深层次的业务逻辑嵌套中中断代码提供了一个很有效的手段。如果没有panic,那么只能在函数的调用的每一层去判断:if err == ErrXXX {return }。这样的代码着实没有什么值得称道的。
对于panic,我觉得要分情况讨论:

  1. 你开发的是个公共的包,提供第三方调用,那么,请勿向公开方法外部抛出任何panic。你可以使用,但是需要记得recover。因为panic是外部难以感知的。调用方很可能没有recover你的panic,那么一旦panic,造成了应用终止,损失是很大的。
  2. 如果你开发的是顶层应用代码,也即是业务代码。那么建议在入口recover所有错误。这样可以避免遭遇异常panic。
  3. go1.14以来,refer的性能已经优化的很好了。如果能用panic节省代码量,方便统一抽象异常处理逻辑,那就用吧,千万不要陷入教条主义。

版权声明

知识共享许可协议 本文章由作者“衡于墨”创作,转载请注明出处,未经允许禁止用于商业用途

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
发布时间:2021年03月11日 15:05:26

评论区#

还没有评论哦,期待您的评论!

关闭特效