[翻译]错误处理和Go

Golang 错误处理的 Panic – Recover 模型确实不太一样,Go 团队的博客上写了一篇相对完整的介绍这个模型使用的文章“Error handling and Go”。我觉得挺好,故翻译于此。本应早就完成这个翻译了,不过由于公司重组等等原因,一直留了首尾没能处理完整。所以拖到了今天,真是不应该啊!

————————翻译分割线————————–

错误处理和Go

如果你已经编写过 Go 代码,可能已经遇到过 os.Error 类型了。Go 代码使用 os.Error 值来标示异常状态。例如,当 os.Open 函数打开文件失败时,返回一个非 nil 的 os.Error 值。

func Open(name string) (file *File, err Error)

下面的函数使用 os.Open 打开一个文件。如果产生了错误,它会调用 log.Fatal 打印错误信息并且中断运行。

func main() {
    f, err := os.Open("filename.ext")
    if err != nil {
        log.Fatal(err)
    }
    // 对打开的 *File f 做些事情
}

在 Go 中只要知道了 os.Error 就可以做很多事情了,不过在这篇文章中,我们会更进一步了解 os.Error 并探讨一些 Go 中错误处理比较好的方法。

错误类型

os.Error 类型是一个接口类型。os.Error变量可以是任何可以将其描绘成字符串的值。这里是接口的定义:

package os

type Error interface {
    String() string
}

对于 os.Error 没什么特别的。只是一个广泛使用的约定而已。

最一般的 os.Error 实现是 os 包的未导出的 errorString 类型。

type errorString string
func (s errorString) String() string { return string(s) }

可以通过 os.NewError 函数构建一个这样的值。它接受一个字符串,然后转换成 os.errorString 并且返回一个 os.Error 值。

func NewError(s string) Error { return errorString(s) }

这里演示了使用 os.NewError 的一种可能:

func Sqrt(f float64) (float64, os.Error) {
    if f < 0 {
        return 0, os.NewError("math: square root of negative number")
    }
    // 实现
}

调用方向 Sqrt 传递了错误的参数,会得到一个非 nil 的 os.Error 值(实际上是重新表达的一个 os.errorString 值)。调用者可以通过调用 os.Error 的 String 方法得到错误字符串,或者仅仅是打印出来:

f, err := Sqrt(-1)
if err != nil {
    fmt.Println(err)
}

fmt 包可以打印任何带有 String() string 方法的内容,这包括了 os.Error 值。

这是错误实现用于概述上下文环境的一种职责。os.Open 返回一个格式化的错误,如“open /etc/passwd: permission denied,”而不仅仅是“permission denied.”Sqrt 返回的错误中缺失了关于非法参数的信息。

为了添加这个信息,在 fmt 包中有一个很有用的函数 Errorf。它将一个字符串依照 Printf 的规则进行格式化,然后将其返回成为 os.NewError 创建的 os.Error 类型。

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

在大多数情况下 fmt.Errorf 已经足够好了,但是由于 os.Error 是一个接口,也可以使用更加详尽的数据结构作为错误值,以便让调用者检查错误的细节。

例如,假设一个使用者希望找到传递到 Sqrt 的非法参数。可以通过定义一个新的错误实现代替 os.errorString 来做到这点:

type NegativeSqrtError float64

func (f NegativeSqrtError) String() string {
    return fmt.Sprintf(“math: square root of negative number %g”, float64(f))
}

一个有经验的调用者可以使用类型断言来检查 NegativeSqrtError 并且特别处理它,仅仅将错误传递给 fmt.Println 或者 log.Fatal 是不会有任何行为上的改变。

另一个例子,json 包指定 json.Decode 函数返回 SyntaxError 类型,当解析一个 JSON blob 发生语法错误的时候。

type SyntaxError struct {
    msg    string // 描述错误
    Offset int64  // 错误在读取了 Offset 字节后发生
}

func (e *SyntaxError) String() string { return e.msg }

Offset 字段没有显示在错误默认的格式中,但是调用者可以使用它来添加文件和行信息到其错误消息中:

if err := dec.Decode(&val); err != nil {
    if serr, ok := err.(*json.SyntaxError); ok {
        line, col := findLine(f, serr.Offset)
        return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
    }
    return err
}

(还有一个略微简单的版本,一些来自Camlistore项目的实际的代码。)

os.Error 接口仅仅需要一个 String 方法;特别的错误实现可能有一些附加的方法。例如,net 包按照惯例返回 os.Error 类型,但是一些错误实现包含由 net.Error 定义的附加方法:

package net

type Error interface {
    os.Error
    Timeout() bool   // 是超时错误吗?
    Temporary() bool // 是临时性错误吗?
}

客户端代码可以用类型断言来测试 net.Error,这样就可以从持久性错误中找到临时性的错误。例如,一个 Web 爬虫可能会在遇到临时性错误时休眠然后重试,持久错误的话就彻底放弃。

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

简化重复的错误处理

在 Go 中,错误处理是重要的。这个语言的设计和规范鼓励对产生错误的地方进行明确的检查(这与其他语言抛出异常,然后在某个时候才处理它们是有区别的)。在某些情况下,这使得 Go 的代码很罗嗦,不过幸运的是有一些让错误处理尽可能少重复的技术可以使用。

考虑 App Engine 应用,在 HTTP 处理时从数据存储获取记录,然后通过模板进行格式化。

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey("Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.String(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.String(), 500)
    }
}

这个函数处理了由 datastore.Get 函数和 viewTemplate 的 Execute 方法返回的错误。在两种情况下,它都是简单的返回一个错误消息给用户,用 HTTP 状态代码 500(“Internal Server Error”)。这代码看起来是可以改进的,只需添加一些 HTTP 处理,然后就可以结束掉这种有许多相同的错误处理代码的状况。

可以自定义 HTTP 处理 appHandler 类型,包括返回一个 os.Error 值来减少重复:

type appHandler func(http.ResponseWriter, *http.Request) os.Error

然后修改 viewRecord 函数返回错误:

func viewRecord(w http.ResponseWriter, r *http.Request) os.Error {
    c := appengine.NewContext(r)
    key := datastore.NewKey("Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

这比原来的版本简单,但是 http 包不明白返回 os.Error 的函数。为了修复这个问题,可以在 appHandler 上实现一个 http.Handler 接口的 ServeHTTP 方法:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.String(), 500)
    }
}

ServeHTTP 方法调用 appHandler 函数,并且给用户显示返回的错误(如果有的话)。注意这个方法的接收者——fn,是一个函数。(Go 可以这样做!)方法调用表达式 fn(w, r) 中定义的接收者。

现在当向 http 包注册了 viewRecord,就可以使用 Handle 函数(代替 HandleFunc)appHandler 作为一个 http.Handler(而不是一个 http.HandlerFunc)。

func init() {
    http.Handle("/view", appHandler(viewRecord))
}

通过这样在基础架构中的错误处理,可以使其对用户更加友好。除了仅仅显示一个错误字符串,给用户一些简单的错误信息以及适当的 HTTP 状态码会更好,同时在 App Engine 开发者控制台记录完整的错误用于调试。

为了做到这点,创建一个 appError 结构包含 os.Error 和一些其他字段:

type appError struct {
    Error os.Error
    Message string
    Code int
}

接下来我们修改 appHandler 类型返回 *appError 值:

type appHandler func(http.ResponseWriter, *http.Request) *appError

(通常,错误信息不使用 os.Error 而是使用实际类型进行传递的做法是错误的,在将发表的文章里会讨论这个,不过在这里是正确的,因为ServeHTTP 是唯一看到这个值并且使用其内容的地方。)

并且编写 appHandler 的 ServeHTTP 方法显示 appError 的 Message 和对应的 HTTP 状态 Code 给用户,同时记录完整的 Error 到开发者控制台:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e is *appError, not os.Error.
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

最后,我们更新 viewRecord 到新的函数声明,并且使其在发生错误的时候返回更多的上下文:

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey("Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

这个版本的 viewRecord 与之前的长度类似,但是现在每行都有特别的含义,并且提供了对用户更加友好的体验。

这还没有结束;还可以进一步在应用中改进错误处理。有一些思路:

  • 为错误处理提供一个漂亮的 HTML 模板,
  • 当用户是管理员时,将栈跟踪输出到 HTTP 的响应中,以方便调试,
  • 编写一个 appError 的构造函数,保存栈跟踪使得调试更容易,
  • 在 appHandler 里从 panic 中 recover,将错误作为“严重异常”记录进开发者控制台,而只简单的告诉用户“发生了一个严重的错误”。 这是避免向用户暴露由于编码错误引起的不可预料的错误的信息的一个不错的想法。参看 Defer, Panic, and Recover 文章了解更多细节。

总结

适当的错误处理是好软件的基本需要。根据本文所讨论的技术,就可以编写出更加可靠和简介的 Go 代码。

Join the Conversation

4 Comments

  1. “通常不使用 os.Error 而是将实际类型作为错误进行传递是错误的”,翻译似乎又问题。

Leave a comment

Your email address will not be published. Required fields are marked *