26

[译]go错误处理

 5 years ago
source link: https://studygolang.com/articles/18507?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

原文来自 Error handling and Go

背景介绍

如果你有写过Go代码,那么你可以会遇到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.etx")
if err != nil {
    log.Fatal(err)
}
// do something with the open *File f

在使用Go的工作中,上面的例子已经能满足大多数情况,但是这篇文章会更进一步的探讨关于捕获异常的实践。

error类型

error类型是一个interface类型。一个error变量可以通过任何可以描述自己的string类型的值来展示自己。下面是它的接口描述:

type error interface {
    Error() String
}

error类型,就像其他内建类型一样,==是在全局中预先声明的==。这意味着我们不用导入就可以在任何地方使用它。

最常用的 error 实现是在 errors 包中定义的一个不可导出的类型: errorString

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

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

通过 errors.New 函数可以创建一个errorString实例.该函数接收一个string参数,并将string参数转换为一个 erros.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, error.New("math: squara root of negative number")
    }
    // implementation
}

在调用Sqrt时,如果传入的参数是负数,调用者会接收到Sqrt返回的一个非空error值(正确来说应该是一个errors.errorString值)。调用者可以通过调用 errorError 方法或者通过打印来得到错误信息字段("math: squara root of nagative number")。

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

fmt 包通过调用Error()方法来格式化 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 的非法参数。我们通过定义一个新的错误实现而不是使用 errors.errorString 来实现这个需求:

type NegativeSqrtError float64

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

一个复杂的调用者就可以使用类型断言(type assertion)来检测 NegativeSqrtError 并且捕获它,与此同时,对于使用 fmt.Println 或者 log.Fatal 来输出错误的方式来说却没有改变他们的行为。

另一个例子来自json包,当我们在使用 json.Decode 函数时,如果我们传入了一个不合格的JSON字段,函数返回 SyntaxError 类型错误。

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甚至还没有在默认的 errorError 函数中出现,但是调用者可以用它来生成带有文件名和行号的错误信息。

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 方法;特定的 error 实现可能会添加其他的一些附加方法。例如 net 包, net 包内有很多种 error 类型,通常跟常用的 error 一样,但是有些 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的语言特性和使用习惯鼓励你在错误发生时做出明确的检测(这和那些抛出异常的然后有时捕获他们的语言有些区别)。在某些情况,这种方式会造成Go代码的冗余,不过幸运的是我们能使用一些技术来减少这种重复的捕获操作。

考虑这样一个App应用,这个应用有一个HTTP的处理函数,用来从数据库接收数据并且将数据用模板格式化。

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

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengin.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormatValue("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 函数和 viewTemplate.Excute 方法返回的错误。这两种情况都返回带Http状态码为500的简单的错误信息。上面的代码看起来也不多,可以接受,但是如果添加更多的 HTTP handlers情况就不一样了,你马上会发现很多这样的重复代码来处理这些错误。

为了减少这些重复的错误处理代码,我们可以定义我们自己的 HTTP AppHandler,让它成一个带着 error 返回值的类型:

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

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

fun viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appending.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValie("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

这看起来比原始版本代码的简单了些, 但是 http 包并不能理解viewRecord函数返回的错误。这时我们可以通过实现在appHandler上的 http.Handler 接口的方法 ServerHTTP 来解决这个问题:

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方法并且将返回的错误展示给用户。注意,ServeHTTP方法的接受者是一个函数。(go语言允许这样做)这个方法通过表达式fn(w, r)来调用他的接受者,使ServeHTTP和appHandler关联在一起

现在,我们在 http 包中注册 viewRecord 时,使用了 Hanlder 函数(而不是HandlerFunc)。因为现在appHandler是一个 http.Handler (而不是 http.HandlerFunc)。

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

通过构建一个特定的 error 作为基础构建,我们可以让我们的错误对用户更友好。相对于仅仅将错误字符串展示给出来,返回带有HTTP状态码的错误字符串是一个更好的展示方式,并且还能记录下所有的错误信息以供App开发者调试用。

下面的代码展示如何实现这种需求。我们创建了一个包含 error 类型的和其他类型的字段的 appError 结构体

type appError struct {
    Error   error
    Message string
    Code    int
}

下一步我们修改 appHandler 类型,让它返回 *appError 值:

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

(通常,相对于返回一个 error 返回一个特定类型的错误是不对的,具体原因可以参考 Go FQA , 但是在这里是正确的,因为这个错误值只有ServeHTTP会用到它)

然后我们让appHandler的 ServeHTTP 方法将带着HTTP状态码的 appError 错误信息展示给用户,并且将所有错误信息展示给开发者终端。

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.ResponseWrite, 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 跟原始版本有着相同的长度,但是现在这些放回信息都有特殊的信息,我们提供了更为友好的用户体验。

当然,这还不是最终的方案,我们还可以进一步提升我们的application中的 error 处理方式。下面是改进的一些点:

  • 给错误handler提供一个漂亮的HTML模板
  • 如果用户是超级用户的话,添加堆叠追踪到HTTP响应中,更方便调试
  • appError 写一个构造函数来存储 stack trace 来让开发者调试更方便
  • 恢复 appHandler 中的panic,用 Critical 级别的log将错误记录到终端,同时告诉用户"a serious error has occurred." 这是一个优雅的方式来避免将程序返回的难以理解的错误暴露给用户。关于panic恢复,读者可以参考 Defer, Panic, and Recover 这篇文章来获取更多的信息。

结论

适合的错误处理是一个好软件最基本的要求。通过这篇文章中讨论的技术,你应该能写出更加可靠简介的Go代码。

参考资料:

Go by Example: Errors


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK