Go之错误处理
作者 fiisio | 发布于 2016-07-01
Go panicrecover

      Go 不像 Java 和 .NET 那样的有着 try/catch 异常处理机制,go不能执行抛异常操作,有了错误就崩溃,一般是严重的错误。但是有自己的一套 defer/panic/recover 机制。

      之前写Java的时候,我特别讨厌的事情就是到处都写try/catch,以至于我都不知道什么时候要写,什么时候不写,所以最后几乎满文件都是try/catch,并且多个嵌套在一起,而且由于是从底层向更高的层级抛异常太耗费资源。此外Java的繁琐也是最不喜欢它的原因(Ps,Java还是有自己的优势的,不过我自己不喜欢它罢了,并不是说他不好)。 Go 错误处理设计的机制也可以 “捕捉” 异常,但是更轻量,并且只作为(处理错误的)最后的手段,也即错误出来了,呢,就是错了,收拾残局就行了。

      Go的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,此时这些运行时错误会引起painc异常。一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括某种错误信息和函数调用的堆栈跟踪信息。并且,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。

Go 如何处理普通错误?

      正常情况我们会通过在函数和方法中返回错误对象作为它们的唯一或最后一个返回值——如果返回 nil,则没有错误发生,并且保证主调(calling)函数总是检查收到的错误。库函数通常总会返回某种错误提示给主调(calling)函数。处理错误并且在发生错误的地方给用户返回错误信息。就算出了问题,你的程序也能继续运行并且通知给用户。在这里要注意:panic /recover 是用来处理真正的异常(无法预测的错误)而不是普通的错误。所以通常我们会这样做:

  • 产生错误的函数会返回两个变量,一个值和一个错误码;如果后者是 nil 就是成功,非 nil 就是发生了错误。
  • 为了防止发生错误时正在执行的函数(如果有必要的话甚至会是整个程序)被中止,在调用函数后必须检查错误。

          示例:

      if value, err := pack1.Func1(param1); err != nil { fmt.Printf(“Error %s in pack1.Func1 with parameter %v”, err.Error(), param1) return // or: return err } // Process(value)

    Go的错误处理是如何实现的?

          Go 有一个预先定义的 error 接口类型,错误值用来表示异常状态;errors 包中有一个 errorString 结构体实现了 error 接口。当程序处于错误状态时可以用 os.Exit(1) 来中止运行。

      type error interface { Error() string }

          定义错误:任何时候当你需要一个新的错误类型,都可以用 errors(必须先 import)包的 errors.New 函数接收合适的错误信息来创建,像这样:

      err := errors.New(“math - square root of negative number”)

          在大部分情况下自定义错误结构类型很有意义的,可以包含除了(低层级的)错误信息以外的其它有用信息,例如,正在进行的操作(打开文件等),全路径或名字。看下面例子中 os.Open 操作触发的 PathError 错误:

      // PathError records an error and the operation and file path that caused it. type PathError struct { Op string // “open”, “unlink”, etc. Path string // The associated file. Err error // Returned by the system call. } func (e *PathError) String() string { return e.Op + “ ” + e.Path + “: “+ e.Err.Error() }

          如果有不同错误条件可能发生,那么对实际的错误使用类型断言或类型判断(type-switch)是很有用的,并且可以根据错误场景做一些补救和恢复操作。

      // err != nil if e, ok := err.(*os.PathError); ok { // remedy situation } 或: switch err := err.(type) { case ParseError: PrintParseError(err) case PathError: PrintPathError(err) ... default: fmt.Printf(“Not a special error, just %s\n”, err) }

          遵循同一种命名规范:错误类型以 “Error” 结尾,错误变量以 “err” 或 “Err” 开头。

    panic和recover的使用

          当发生像数组下标越界或类型断言失败这样的运行错误时,Go 运行时会触发运行时 panic,伴随着程序的崩溃抛出一个 runtime.Error 接口类型的值。这个错误值有个 RuntimeError() 方法用于区别普通错误。

          panic 可以直接从代码初始化:当错误条件(我们所测试的代码)很严苛且不可恢复,程序不能继续运行时,可以使用 panic 函数产生一个中止程序的运行时错误。panic 接收一个做任意类型的参数,通常是字符串,在程序死亡时被打印出来。Go 运行时负责中止程序并给出调试信息。

          当发生错误必须中止程序时,panic 可以用于错误处理模式:

      if err != nil { panic(“ERROR occurred:” + err.Error()) }

          在多层嵌套的函数调用中调用 panic,可以马上中止当前函数的执行,所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样可以向上冒泡直到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况:这个终止过程就是 panicking。

          标准库中有许多包含 Must 前缀的函数,像 regexp.MustComplie 和 template.Must;当正则表达式或模板中转入的转换字符串导致错误时,这些函数会 panic。但是,不能随意地用 panic 中止程序,必须尽力补救错误让程序能继续执行。

          通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。recover内建函数被用于从 panic 或 错误场景中恢复:让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行。recover 只能在 defer 修饰的函数中使用,用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。panic 会导致栈被展开直到 defer 修饰的 recover() 被调用或者程序中止。这跟 Java 和 .NET 这样的语言中的 catch 块类似。但有些情况下,我们无法恢复。某些致命错误会导致Go在运行时终止程序,如内存不足。

          这是一个展示 panic,defer 和 recover 怎么结合使用的完整例子:

          示例:

      // panic_recover.go package main import ( "fmt" ) func badCall() { panic("bad end") } func test() { defer func() { if e := recover(); e != nil { fmt.Printf("Panicing %s\r\n", e) } }() badCall() fmt.Printf("After bad call\r\n") // <-- wordt niet bereikt } func main() { fmt.Printf("Calling test\r\n") test() fmt.Printf("Test completed\r\n") } 输出: Calling test Panicing bad end Test completed

          自定义包中错误处理实现者应该遵守的最佳实践:

  • 在包内部,总是应该从 panic 中 recover:不允许显式的超出包范围的 panic()
  • 向包的调用者返回错误值(而不是 panic)

          在包内部,特别是在非导出函数中有很深层次的嵌套调用时,对主调函数来说用 panic 来表示错误的场景是很有用。

      // parse.go package parse import ( "fmt" "strings" "strconv" ) // A ParseError indicates an error in converting a word into an integer. type ParseError struct { Index int // The index into the space-separated list of words. Word string // The word that generated the parse error. Err error // The raw error that precipitated this error, if any. } // String returns a human-readable error message. func (e *ParseError) String() string { return fmt.Sprintf("pkg parse: error parsing %q as int", e.Word) } // Parse parses the space-separated words in in put as integers. func Parse(input string) (numbers []int, err error) { defer func() { if r := recover(); r != nil { var ok bool err, ok = r.(error) if !ok { err = fmt.Errorf("pkg: %v", r) } } }() fields := strings.Fields(input) numbers = fields2numbers(fields) return } func fields2numbers(fields []string) (numbers []int) { if len(fields) == 0 { panic("no words to parse") } for idx, field := range fields { num, err := strconv.Atoi(field) if err != nil { panic(&ParseError{idx, field, err}) } numbers = append(numbers, num) } return } // panic_package.go package main import ( "fmt" "./parse/parse" ) func main() { var examples = []string{ "1 2 3 4 5", "100 50 25 12.5 6.25", "2 + 2 = 4", "1st class", "", } for _, ex := range examples { fmt.Printf("Parsing %q:\n ", ex) nums, err := parse.Parse(ex) if err != nil { fmt.Println(err) // here String() method from ParseError is used continue } fmt.Println(nums) } } 输出: Parsing "1 2 3 4 5": [1 2 3 4 5] Parsing "100 50 25 12.5 6.25": pkg parse: error parsing "12.5" as int Parsing "2 + 2 = 4": pkg parse: error parsing "+" as int Parsing "1st class": pkg parse: error parsing "1st" as int Parsing "": pkg: no words to parse