Go 语言异常处理

Go 有一个预先定义的 error 接口类型

  1. type error interface {
  2. Error() string
  3. }

errors 包中有一个 errorString 结构体实现了 error 接口。当程序处于错误状态时可以用 os.Exit(1) 来中止运行。

定义异常

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

  1. err := errors.New("math - square root of negative number")

在下面的示例中你可以看到一个简单的用例:

示例 errors.go:

  1. // errors.go
  2. package main
  3. import (
  4. "errors"
  5. "fmt"
  6. )
  7. var errNotFound error = errors.New("Not found error")
  8. func main() {
  9. fmt.Printf("error: %v", errNotFound)
  10. }
  11. // error: Not found error

可以把它用于计算平方根函数的参数测试:

  1. func Sqrt(f float64) (float64, error) {
  2. if f < 0 {
  3. return 0, errors.New ("math - square root of negative number")
  4. }
  5. // implementation of Sqrt
  6. }

你可以像下面这样调用 Sqrt 函数:

  1. if f, err := Sqrt(-1); err != nil {
  2. fmt.Printf("Error: %s\n", err)
  3. }

由于 fmt.Printf 会自动调用 String() 方法,所以错误信息 “Error: math - square root of negative number” 会打印出来。通常(错误信息)都会有像 “Error:” 这样的前缀,所以你的错误信息不要以大写字母开头。

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

  1. // PathError records an error and the operation and file path that caused it.
  2. type PathError struct {
  3. Op string // "open", "unlink", etc.
  4. Path string // The associated file.
  5. Err error // Returned by the system call.
  6. }
  7. func (e *PathError) Error() string {
  8. return e.Op + " " + e.Path + ": "+ e.Err.Error()
  9. }

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

  1. // err != nil
  2. if e, ok := err.(*os.PathError); ok {
  3. // remedy situation
  4. }
  5. //或:
  6. switch err := err.(type) {
  7. case ParseError:
  8. PrintParseError(err)
  9. case PathError:
  10. PrintPathError(err)
  11. ...
  12. default:
  13. fmt.Printf("Not a special error, just %s\n", err)
  14. }

作为第二个例子考虑用 json 包的情况。当 json.Decode 在解析 JSON 文档发生语法错误时,指定返回一个 SyntaxError 类型的错误:

  1. type SyntaxError struct {
  2. msg string // description of error
  3. // error occurred after reading Offset bytes, from which line and columnnr can be obtained
  4. Offset int64
  5. }
  6. func (e *SyntaxError) Error() string { return e.msg }

在调用代码中你可以像这样用类型断言测试错误是不是上面的类型:

  1. if serr, ok := err.(*json.SyntaxError); ok {
  2. line, col := findLine(f, serr.Offset)
  3. return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
  4. }

包也可以用额外的方法(methods)定义特定的错误,比如 net.Error

  1. package net
  2. type Error interface {
  3. Timeout() bool // Is the error a timeout?
  4. Temporary() bool // Is the error temporary?
  5. }

正如你所看到的一样,所有的例子都遵循同一种命名规范:错误类型以 “Error” 结尾,错误变量以 “err”“Err” 开头。

syscall 是低阶外部包,用来提供系统基本调用的原始接口。它们返回封装整数类型错误码的syscall.Errno,类型 syscall.Errno 实现了 Error 接口。

大部分 syscall 函数都返回一个结果和可能的错误,比如:

  1. r, err := syscall.Open(name, mode, perm)
  2. if err != nil {
  3. fmt.Println(err.Error())
  4. }

os 包也提供了一套像 os.EINAL 这样的标准错误,它们基于 syscall 错误:

  1. var (
  2. EPERM Error = Errno(syscall.EPERM)
  3. ENOENT Error = Errno(syscall.ENOENT)
  4. ESRCH Error = Errno(syscall.ESRCH)
  5. EINTR Error = Errno(syscall.EINTR)
  6. EIO Error = Errno(syscall.EIO)
  7. ...
  8. )

用 fmt 创建异常对象

通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf() 来实现:它和 fmt.Printf() 完全一样,接收一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。

比如在前面的平方根例子中使用:

  1. if f < 0 {
  2. return 0, fmt.Errorf("math: square root of negative number %g", f)
  3. }

第二个例子:从命令行读取输入时,如果加了 help 标志,我们可以用有用的信息产生一个错误:

  1. if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
  2. err = fmt.Errorf("usage: %s infile.txt outfile.txt", filepath.Base(os.Args[0]))
  3. return
  4. }

运行时异常和 panic

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

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

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println("Starting the program")
  5. panic("A severe error occurred: stopping the program!")
  6. fmt.Println("Ending the program")
  7. }

输出如下:

  1. Starting the program
  2. panic: A severe error occurred: stopping the program!
  3. panic PC=0x4f3038
  4. runtime.panic+0x99 /go/src/pkg/runtime/proc.c:1032
  5. runtime.panic(0x442938, 0x4f08e8)
  6. main.main+0xa5 E:/Go/GoBoek/code examples/chapter 13/panic.go:8
  7. main.main()
  8. runtime.mainstart+0xf 386/asm.s:84
  9. runtime.mainstart()
  10. runtime.goexit /go/src/pkg/runtime/proc.c:148
  11. runtime.goexit()
  12. ---- Error run E:/Go/GoBoek/code examples/chapter 13/panic.exe with code Crashed
  13. ---- Program exited with code -1073741783

下面是一个检查程序是否被已知用户启动的具体例子:

  1. var user = os.Getenv("USER")
  2. func check() {
  3. if user == "" {
  4. panic("Unknown user: no value for $USER")
  5. }
  6. }

可以在导入包的 init() 函数中检查这些。当发生错误必须中止程序时,panic 可以用于错误处理模式:

  1. if err != nil {
  2. panic("ERROR occurred:" + err.Error())
  3. }
  4. Go panicking

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

标准库中有许多包含 Must 前缀的函数,像 regexp.MustComplietemplate.Must,当正则表达式或模板中转入的转换字符串导致错误时,这些函数会 panic

不能随意地用 panic 中止程序,必须尽力补救错误让程序能继续执行。

从panic中恢复(Recover)

正如名字一样,这个(recover)内建函数被用于从 panic错误场景中恢复,让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行。

recover 只能在 defer 修饰的函数中使用,用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。

总结:panic 会导致栈被展开直到 defer 修饰的 recover() 被调用或者程序中止。

下面例子中的 protect 函数调用函数参数 g 来保护调用者防止从 g 中抛出的运行时 panic,并展示 panic 中的信息:

  1. func protect(g func()) {
  2. defer func() {
  3. log.Println("done")
  4. // Println executes normally even if there is a panic
  5. if err := recover(); err != nil {
  6. log.Printf("run time panic: %v", err)
  7. }
  8. }()
  9. log.Println("start")
  10. g() // possible runtime-error
  11. }

这跟 Java.NET这样的语言中的 catch 块类似。

Go 标准库中许多地方都用了这个机制,例如,json 包中的解码和 regexp 包中的 Complie 函数。Go 库的原则是即使在包的内部使用了 panic,在它的对外接口(API)中也必须用 recover 处理成返回显式的错误。