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
操作,导致其他问题
- 在一些特定场景下,比如web服务器中,不可能因为一个处理函数触发了
综上所述的使用
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运行时内存不足,则会直接终止程序
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。