Go 中的错误处理

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

Andrew Gerrand
2011 年 7 月 12 日

介绍

如果你编写过任何 Go 代码, 则可能会遇到过内置的 error 类型. Go 代码使用 error 值来表示异常状态. 例如, os.Open 函数在无法打开一个文件时会返回非零 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)
}
// 打开 *File f 后干点什么

你可以在 Go 中了解到更多关于 error 类型的内容, 但在本文中, 我们将着重于 error 并讨论一些错误处理的好方法.

错误类型

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

type error interface {
    Error() string
}

跟所有内置类型一样, error 类型在 universe block 中是 predeclared.

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

// errorString 是 error 的简单实现.
type errorString struct {
    s string
}

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

你可以使用 errors.New 函数来构造这些值. 其接收一个字符串并将字符串转化为 errors.errorString 并作为 error 值返回.

// New 函数返回一个给定文本的错误
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")
    }
    // 实现
}

调用者将负参数传递给 Sqrt 比接收一个非零的 error 值 (其具体表现为 errors.errorString 值). 调用者可以通过调用 errorError 函数来访问或仅打印错误字符串 (“math: square root of negative number”):

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 二进制数据时遇到的语法错误.

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

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   // 是否超时错误?
    Temporary() bool // 是否暂时性错误?
}

客户端代码可以使用类型声明来测试 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) 中调用接收者来调用函数.

现在当我们使用 Handle 函数 (HandleFunc 函数的替代) 注册 viewRecord 函数时, 使用 Handle 函数使 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 方法具有向正确 HTTP 状态 Code 的用户显示 appErrorMessage, 并将完整的 Error 记录到开发者控制台:

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e 是 *appError, 不是 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 内部的恐慌状态中恢复, 并将错误记录为 “严重”, 同时告知用户 “发生了严重错误”. 这样可以很好的避免用户暴露由编程错误引起的难以理解的错误信息. 有关更多信息, 请参阅 Defer, Panic, and Recover.

总结

正确的错误处理是好软件的基本要求. 通过采用本文描述的技术, 你应该能够编写更加可靠和简洁的 Go 代码了.

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/go-blog/error-h...

译文地址:https://learnku.com/docs/go-blog/error-h...

上一篇 下一篇
Summer
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~