Go异样解决

谬误与异样解决

  • 要辨别Go中的错误处理与异样解决

    • 错误处理指的是函数通过返回谬误的模式(个别是error类型的返回值)向调用方传递错误信息的形式,这种错误信息代表了函数运行的失败,但并不被认为是程序的异样,而是函数预期可能呈现的后果,能够进一步的解决,返回给用户或者执行重试等等,以晋升程序鲁棒性
    • 异样机制被用来解决未曾构想的谬误,也就是bug,而不是强壮程序中失常呈现的谬误。Go中异样与谬误的比照相似于Java中的非运行时异样与运行时异样的区别

      • Java中的非运行时异样强制捕捉,因为其针对的是一些预期呈现的异样,强制性的捕捉能够晋升程序的鲁棒性,然而运行时异样针对的则是一些显著的逻辑谬误也就是bug,比方数组越界等等,这些异样不强制捕捉,为的就是尽可能的裸露给程序员,以供修复

panic/recover

  • Go语言应用panic/recover模式来处理错误

panic

应用场景
  • panic能够在任何中央触发,触发后程序中断,立刻执行以后goroutine中定义的defer语句,随后,程序解体并输入日志信息。日志信息包含panic value(错误信息)和函数调用的堆栈跟踪信息(对于每个goroutine,日志信息中都会有与之绝对的,产生panic时的函数调用堆栈跟踪信息),比方下边的例子

    panic: runtime error: integer divide by zerogoroutine 1 [running]:github.com/JJLAAA/base/func/function.PanicTest(...)        /Users/lijia/GoProjects/GoProject/HelloWorld/github.com/JJLAAA/base/func/function/panicTest.go:4main.main()        /Users/lijia/GoProjects/GoProject/HelloWorld/github.com/JJLAAA/base/func/main.go:6 +0x12
    • 对于defer的执行机会须要留神的是,定义在可能触发panic异样的语句之后的defer语句并不会在异常中断后被执行,只有定义在之前的defer语句会执行
    • runtime包提供了Stack办法用来输入堆栈信息(不蕴含panic value,以更不便的诊断问题

      func main() {      defer printStack()    f(3)}func f(x int) {    fmt.Printf("f(%d)\n", x+0/x) // panics if x == 0    defer fmt.Printf("defer %d\n", x)    f(x - 1)}func printStack() {    var buf [4096]byte    n := runtime.Stack(buf[:], false)    os.Stdout.Write(buf[:n])}
      • 程序解体后,首先应用printStack打印堆栈信息,而后再打印panic value + 堆栈信息,在Go的panic机制中,提早函数的调用在开释堆栈信息之前,所以Stack办法能在函数堆栈开释前获取其信息
    • Go同时提供了recover内置函数使得程序从panic异样中复原,以阻止程序解体
  • panic异样有两种触发形式

    • 运行时的异样触发,比方做除法运算时,除数是0,则会在运行时触发panic异样

      func PanicTest(div int) {    print(1 / div)}func main() {    function.PanicTest(0)}
      panic: runtime error: integer divide by zero
    • 应用内置的panic函数手动触发异样,个别在逻辑上不应该产生的场景中调用此panic,以起到debug的作用
应用准则
  • 一个强壮的程序内应该尽可能的应用Go提供的谬误机制解决预期的谬误,而尽量减少panic的应用,panic个别用于严重错误,如程序外部的逻辑不统一等等
  • Go语言的源码包中提供了一种Must为前缀的函数,这些函数的设计认为适度的应用谬误机制也是不适合的,比方《Go语言圣经》中给到了regexp.MustCompile的例子,对该类型的函数的设计了解是:当调用者明确的晓得正确的输出不会引起函数谬误时,要求调用者查看这个谬误是不必要和累赘的。咱们应该假如函数的输出始终非法,当调用者输出了不应该呈现的输出时,触发panic异样。函数名中的Must前缀是一种针对此类函数的命名约定,示意要求调用者必须提供正确的参数,否则触发panic

    func MustCompile(str string) *Regexp {    regexp, err := Compile(str)    if err != nil {        panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())    }    return regexp}
    • 再举一个html/template包下的Must办法的例子

      var report = template.Must(template.New("issuelist").    Funcs(template.FuncMap{"daysAgo": daysAgo}).    Parse(templ))func Must(t *Template, err error) *Template {    if err != nil {        panic(err)    }    return t}
      • Must函数实际上是一种断言机制,如果Parse函数返回了谬误则代表参数t有效,此时触发panic异样
  • 个别库在提供Must类型的函数的同时也会提供非Must类型的对应函数,这类函数则应用谬误机制解决,例如regexp.Compile

    func Compile(expr string) (*Regexp, error) {    return compile(expr, syntax.Perl, false)}

recover

应用场景
  • 只管panic异样的目标在于揭发程序中的bug,仿佛对于该异样,不必做任何解决,只须要等着他触发并查看异样信息即可,然而实际上在理论生产测试中,如果万一呈现了panic异样,还是须要在此时做一些必要的解救伎俩,比方:Web程序中遇到重大的panic异样时,应该在解体前敞开所有连贯,防止客户端期待,在开发测试阶段,甚至还能够将异样信息返回到客户端。因而,Go同时提供了recover内置函数使得程序从panic异样中复原
  • recover该当在引发panic的语句之前定义的defer语句中被调用,此时如果出现异常,以后函数将不再执行,然而能够失常返回,而不至于引发程序解体panic value与函数堆栈信息也不会被打印
  • 如果没有出现异常的状况下调用recover函数或者是panic函数传入nil或者是未在defer语句中调用recover的状况下,recover函数的返回值是nil,否则recover函数返回panic函数传入的参数信息或者是运行时给的panic value
  • goroutine中有多个defer(recover)语句时,panic异样最近的defer(recover)语句将解决异样

    func RecoverTest(div int) {    defer func() {        fmt.Println(recover())    }()    fmt.Println(1 / div)}
    import (    "fmt"    "github.com/JJLAAA/base/func/function")func main() {    defer func() {        fmt.Println(recover())    }()    function.RecoverTest(0)    println("main end")}
    • RecoverTest办法中的recover语句会率先执行,而main办法中的defer语句则不会取得无效的panic value,只会取得nil
应用准则
  • recover函数能够实现panic异样到error的转化,从而实现程序的复原,见上面的例子,然而思考到二者的触发场景不同,不加区分的复原所有的panic异样,不是可取的做法,因为在panic之后,无奈保障包级变量的状态依然和咱们预期统一。比方,对数据结构的一次重要更新没有被残缺实现、文件或者网络连接没有被敞开、取得的锁没有被开释。此外,如果写日志时产生的panic被不加区分的复原,可能会导致破绽被疏忽。

    func Parse(input string) (s *Syntax, err error) {    defer func() {        if p := recover(); p != nil {            err = fmt.Errorf("internal error: %v", p)        }    }()    // ...parser...}
  • 一般来说,不应该试图去复原其余包引起的panic。私有的API应该将函数的运行失败作为error返回,而不是panic。同样的,也不应该复原一个由别人开发的函数引起的panic,比如说调用者传入的回调函数,因为无奈确保这样做是平安的

    • 在一些特定场景下,比方web服务器中,不可能因为一个处理函数触发了panic异样,就间接关停服务器,web服务器遇到处理函数导致的panic时会调用recover,输入堆栈信息,持续运行。这样的做法在实践中很便捷,但也会引起资源透露,或是因为recover操作,导致其余问题
  • 综上所述的应用recover解决panic异样的种种问题,平安的做法是有选择性的recover,只复原应该被复原的panic异样。为了标识某个panic是否应该被复原,能够将panic value设置成非凡类型。在recover时对panic value进行查看,如果发现panic value是非凡类型,就将这个panic作为error解决,如果不是,则依照失常的panic进行解决

    // soleTitle returns the text of the first non-empty title element// in doc, and an error if there was not exactly one.func soleTitle(doc *html.Node) (title string, err error) {    type bailout struct{}    defer func() {        switch p := recover(); p {        case nil:       // no panic        case bailout{}: // "expected" panic            err = fmt.Errorf("multiple title elements")        default:            panic(p) // unexpected panic; carry on panicking        }    }()    // Bail out of recursion if we find more than one nonempty title.    forEachNode(doc, func(n *html.Node) {        if n.Type == html.ElementNode && n.Data == "title" &&            n.FirstChild != nil {            if title != "" {                panic(bailout{}) // multiple titleelements            }            title = n.FirstChild.Data        }    }, nil)    if title == "" {        return "", fmt.Errorf("no title element")    }    return title, nil}
    • 上述代码是页面爬虫提取Title的办法,当发现有多个Title时会触发panic,并传入bailout类型的panic value,此时则能够将其转化为error返回,而其余未知状况则仍触发panic异样

      • 实际上这种已知状况的异样更适宜间接用error示意,这里仅做演示
  • 当然,在非凡状况下,recover也无奈复原panic异样后解体的程序,比方Go运行时内存不足,则会间接终止程序

参考

  • Go语言圣经