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 运行时内存不足,则会间接终止程序
参考
- Go 语言圣经