Go 博客

错误处理和 Go

Andrew Gerrand
2011 年 7 月 12 日

简介

如果您编写过任何 Go 代码,您可能已经遇到过内置的 error 类型。Go 代码使用 error 值来指示异常状态。例如,当 os.Open 函数无法打开文件时,它会返回一个非 nil 的 error 值。

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

以下代码使用 os.Open 打开一个文件。如果发生错误,它会调用 log.Fatal 打印错误消息并停止。

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

仅了解 error 类型的这些信息,您就可以在 Go 中完成很多工作,但在本文中,我们将更深入地了解 error,并讨论一些在 Go 中进行错误处理的良好实践。

error 类型

error 类型是一个接口类型。error 变量表示任何可以将自身描述为字符串的值。以下是接口的声明

type error interface {
    Error() string
}

error 类型与所有内置类型一样,在预声明标识符宇宙块中进行了预声明

最常用的 error 实现是 errors 包的未导出 errorString 类型。

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

您可以使用 errors.New 函数构造其中一个值。它接受一个字符串,将其转换为 errors.errorString 并作为 error 值返回。

// New returns an error that formats as the given text.
func New(text string) error {
    return &errorString{text}
}

以下是如何使用 errors.New 的示例

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // implementation
}

将负参数传递给 Sqrt 的调用方会收到一个非 nil 的 error 值(其具体表示形式为 errors.errorString 值)。调用方可以通过调用 errorError 方法或仅打印它来访问错误字符串(“math: square root of…”)。

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

fmt 包通过调用其 Error() string 方法来格式化 error 值。

错误实现有责任总结上下文。os.Open 返回的错误格式为“open /etc/passwd: permission denied”,而不仅仅是“permission denied”。我们的 Sqrt 返回的错误缺少有关无效参数的信息。

为了添加该信息,一个有用的函数是 fmt 包的 Errorf。它根据 Printf 的规则格式化字符串,并将其作为由 errors.New 创建的 error 返回。

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

在许多情况下,fmt.Errorf 足够好了,但由于 error 是一个接口,因此您可以使用任意数据结构作为错误值,以允许调用方检查错误的详细信息。

例如,我们假设的调用方可能希望恢复传递给 Sqrt 的无效参数。我们可以通过定义一个新的错误实现而不是使用 errors.errorString 来实现这一点

type NegativeSqrtError float64

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

然后,复杂的调用方可以使用类型断言来检查 NegativeSqrtError 并对其进行特殊处理,而仅将错误传递给 fmt.Printlnlog.Fatal 的调用方将不会看到行为上的任何变化。

作为另一个示例,json 包指定了一个 SyntaxError 类型,当 json.Decode 函数在解析 JSON Blob 时遇到语法错误时,它会返回该类型。

type SyntaxError struct {
    msg    string // description of error
    Offset int64  // error occurred after reading Offset bytes
}

func (e *SyntaxError) Error() 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 项目的一些实际代码的稍微简化版本。)

error 接口仅需要一个 Error 方法;特定的错误实现可能具有其他方法。例如,net 包返回 error 类型的错误,遵循通常的约定,但一些错误实现具有由 net.Error 接口定义的其他方法

package net

type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

客户端代码可以使用类型断言来测试 net.Error,然后区分短暂网络错误和永久性错误。例如,网络爬虫在遇到临时错误时可能会休眠并重试,否则会放弃。

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

简化重复的错误处理

在 Go 中,错误处理非常重要。该语言的设计和约定鼓励您在错误发生的地方显式检查错误(不同于其他语言中抛出异常并有时捕获它们的约定)。在某些情况下,这会使 Go 代码冗长,但幸运的是,您可以使用一些技术来最大程度地减少重复的错误处理。

考虑一个具有 HTTP 处理程序的App Engine 应用程序,该处理程序从数据存储区检索记录并使用模板对其进行格式化。

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

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

此函数处理 datastore.Get 函数和 viewTemplateExecute 方法返回的错误。在这两种情况下,它都会向用户显示一条简单的错误消息,并使用 HTTP 状态代码 500(“内部服务器错误”)。这看起来像是可管理数量的代码,但是添加更多 HTTP 处理程序,您很快就会得到许多相同错误处理代码的副本。

为了减少重复,我们可以定义自己的 HTTP appHandler 类型,其中包含一个 error 返回值

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

然后,我们可以更改 viewRecord 函数以返回错误

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "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 包不理解返回 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.Error(), 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 结构体,其中包含一个 error 和一些其他字段

type appError struct {
    Error   error
    Message string
    Code    int
}

接下来,我们将 appHandler 类型修改为返回 *appError

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

(通常,出于Go 常见问题解答中讨论的原因,传递错误的具体类型而不是 error 是一个错误,但在这里这样做是正确的,因为 ServeHTTP 是唯一看到该值并使用其内容的地方。)

并使 appHandlerServeHTTP 方法向用户显示 appErrorMessage,并使用正确的 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(c, "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 内部的恐慌中恢复,将错误记录到控制台为“严重”,同时告诉用户“发生了严重错误”。这是一个很好的方法,可以避免向用户公开由编程错误引起的难以理解的错误消息。有关更多详细信息,请参阅延迟、恐慌和恢复文章。

结论

正确的错误处理是良好软件的基本要求。通过使用本文中描述的技术,您应该能够编写更可靠和简洁的 Go 代码。

下一篇文章:Go for App Engine 现已普遍可用
上一篇文章:Go 中的一级函数
博客索引