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 zero
    
    goroutine 1 [running]:
    github.com/JJLAAA/base/func/function.PanicTest(...)
            /Users/lijia/GoProjects/GoProject/HelloWorld/github.com/JJLAAA/base/func/function/panicTest.go:4
    main.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运行时内存不足,则会直接终止程序

参考


demoli
16 声望0 粉丝

bug创建者