Go 和错误处理

Go 和错误处理

简介

如果你写过 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 变量可以表示任何可以用字符串描述自身的值。它的接口声明:

type error interface {
    Error() string
}
复制代码

错误类型和其他内置类型一样,被预定义了。

最常见的 error 接口实现是标准库 errors 包中未导出的 errorString 类型。

// errorString 是 error 的简单实现
type errorString struct {
    s string
}
​
func (e *errorString) Error() string {
    return e.s
}
复制代码

你可以用 errors.New 函数来创建一个 error 值。函数接收一个字符串并将字符串转换为 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 值(具体类型是 errorString)。调用者可以通过 errorError方法来访问和打印错误信息(“math: square root of…")。

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

fmt 包通过调用 Error() string 函数来格式化 error 值。

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是一个接口类型,我们当然可以用任意的类型来作为 error 值,如此一来就能让调用者检测到错误的更多细节。

例如,我们假定调用者想要恢复传入 Sqrt 的无效参数。我们可以通过定义一个新的 error 实现来代替 errors.errorString

type NegativeSqrtError float64func (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
}
复制代码

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 的设计和约定都鼓励你在错误可能发生的位置去显示地检查(与其他语言中抛出异常并捕获的约定有所不同)。在一些情况下,这会使得 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.GetviewTemplate 返回的错误。这两个情况下,程序会给用户展现一些简单的错误信息和 HTTP 状态码 500(“Internal Server Error”)。看起来代码数量是可控的,但是在添加更多的 HTTP 处理程序时,你很快会将同样的错误处理代码重复多次。

为了避免这种重复,我们可以自己定义 HTTP appHandler 类型,它包含一个 error类型的返回值:

type appHandler func(http.ResponseWriter, *http.Request) error
复制代码

然后我们可以修改我们的 viewRecord 函数,返回一个 error

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) 中调用接收者来调用函数。

现在,当我们注册 viewRecord 函数时,将使用 http 包中的 Handle 函数(而不是 HandleFunc)将 appHandler 作为 http.Handler 来处理(而不是 http.HandlerFunc)。

func init() {
    http.Handle("/view", appHandler(viewRecord))
}
复制代码

有了这个基本的错误处理结构,我们可以进一步让它变得对用户更友好。和仅仅展示错误字符串信息相比,给用户提供带有适当 HTTP 状态码的简单错误信息,同时将完整的错误记录到 App Engine 开发者控制台来进行调试对用户而言更加友好。

为此我们构建了一个包含一个 error 和一些其他字段的 appError 结构体:

type appError struct {
    Error   error
    Message string
    Code    int
}
复制代码

接着我们修改 appHnadler 的定义,返回 *appError

type appHandler func(http.ResponseWriter, *http.Request) *appError
复制代码

(通常来说,传回错误的具体类型而不是 error 是个错误,原因在Go FAQ中讨论过,但在这里做的是正确的,因为 ServeHTTP 是唯一能看到这个值并使用其内容的地方。)

并使得 appHandler 实现的 ServeHTTP 方法以正确的 HTTP 状态代码向用户显示 appError 的信息,并将完整的 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 代码。

翻译自:go blog 《Error handling and Go

猜你喜欢

转载自juejin.im/post/7131238192996417566