5.14. 错误处理

一些函数在调用后一般会返回一些错误的标志。在Go中我们可以用返回返回多个值来 方便地处理错误标志信息。一般情况下,错误都实现了os.Error 接口。

  type Error interface {
      String() string
  }

库的编写者一般会在os.Error接口的基础上扩展更多的信息,这样函数调用者 可以知道错误的更多细节。例如:os.Open返回的是os.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.
      Error Error  // Returned by the system call.
  }

  func (e *PathError) String() string {
      return e.Op + " " + e.Path + ": " + e.Error.String()
  }

PathError生成的String错误信息如下:

  open /etc/passwx: no such file or directory

这个错误信息包含了要操作的文件名,对文件的具体操作,以及操作系统返回的错误信息。 这样肯定比简单输出"no such file or directory"错误信息更有价值。

如果函数调用者想获取错误的全部细节,那么需要将错误结果从基本的类型动态转换到 更具体的错误类型。例如:下面的代码将Error转换为PathErrors 类型,因为后者的错误细细更加丰富。

  for try := 0; try < 2; try++ {
      file, err = os.Open(filename, os.O_RDONLY, 0)
      if err == nil {
          return
      }
      if e, ok := err.(*os.PathError); ok && e.Error == os.ENOSPC {
          deleteTempFiles()  // Recover some space.
          continue
      }
      return
  }

5.14.1. Panic(怕死)

通常报错的方式是给调用者一个多于的 os.Error 的返回值。经典的Read 方法是个出名的实例;它返回字节数和 os.Error 。但错误不可恢复则如何?有时程序就是不可再继续了。

基于此目的,内部函数 panic 实际上会生成一个运行态错误来终止程序(但参见下节)。此函数取一个任意类型的参量 —— 通常是字串——在程序死掉时打印。它也用来指出某种不可能的事情发生了,例http://code.google.com/p/ac-me/ 99如从永久循环中退出了。实际上,编辑器看到函数尾的 panic 会压制通常的 return 语句检查。

  // A toy implementation of cube root using Newton's method.
  func CubeRoot(x float64) float64 {
      z := x/3   // Arbitrary intitial value
      for i := 0; i < 1e6; i++ {
          prevz := z
          z -= (z*z*z-x) / (3*z*z)
          if veryClose(z, prevz) {
              return z
          }
      }
      // A million iterations has not converged; something is wrong.
      panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
  }

这只是个例子,实际的库函数要避免使用 panic 。如果某问题可被屏蔽或绕过,最好让事情继续而不是打死整个程序。一个可能的反例是在初始化时,如果某库函数怎么都不能安排好自己,有理由 panic,可以这么说。

  var user = os.Getenv("USER")

  func init() {
      if user == "" {
          panic("no value for $USER")
      }
  }

5.14.2. Recover(回生)

当 panic 被叫,包括运行态错误例如数组下标越界或类型断言失败时,它会立即停止当前函数的执行,并开始退绕够程的堆栈,随之运行所有的延迟函数。如果退绕到够程堆栈顶,程序死掉。但是,我们可以用内部函数 recover 重新控制够程,恢复正常运行。

recover 的调用终止退绕并返回传给 panic 的参量。因为退绕时只有延迟函数的代码在运行,recover 只在延迟函数有用。

recover 的一个用途是在服务器内关闭失败的够程而不会杀死其它正在运行的够程。

  func server(workChan <-chan *Work) {
      for work := range workChan {
          go safelyDo(work)
      }
  }

  func safelyDo(work *Work) {
      defer func() {
          if err := recover(); err != nil {
              log.Stderr("work failed:", err)
          }
      }()
      do(work)
  }

此例子中,如果 do(work) panic了,结果会记录下,够程会不扰人的干净地退出。没必要在延迟函数做其它的事;recover 的调用完全可以处理。

意有了这种复原的模式,do 函数(及其所有的调用)可以用 panic 从任何糟糕的情况里脱身。我们可用此概念简化复杂软件的出错处理。我们看看 regexp 包里一个理想化的节选,它用局部的 Error 类型调用 panic 来报错。 下面是 Error,error 方法,和 Compile 函数的定义:

  // Error is the type of a parse error; it satisfies os.Error.
  type Error string
  func (e Error) String() string {
      return string(e)
  }

  // error is a method of *Regexp that reports parsing errors by
  // panicking with an Error.
  func (regexp *Regexp) error(err string) {
      panic(Error(err))
  }

  // Compile returns a parsed representation of the regular expression.
  func Compile(str string) (regexp *Regexp, err os.Error) {
      regexp = new(Regexp)
      // doParse will panic if there is a parse error.
      defer func() {
          if e := recover(); e != nil {
              regexp = nil    // Clear return value.
              err = e.(Error) // Will re-panic if not a parse error.
          }
      }()
      return regexp.doParse(str), nil
  }

如果 doParse panic了,复原块会设置返回值为 nil ——延迟函数可以修改带名的返回值。它然后通过断定 err 的赋值是类型 Error 来检查问题出自语法分析。如果不是,类型断言会失败,导致一个运行态错误,继续堆栈退绕,就好像无事发生一样。这个检查意味着如果未曾预料的事情发生了,例如数组下标越界,代码会失败,尽管我们用了panic 和 recover 出来用户触发的错误。

有了这种出错处理,error 方法能轻易的报错,而不需担心自己动手退绕堆栈。

这种有用的模式只应在一个包的内部使用。 Parse 将其内部的 panic 调用转为 os.Error 值;不把 panic 暴露给客户。这个好规则值得效法。