[译]go错误处理

原文来自Error handling and Gogit

背景介绍

若是你有写过Go代码,那么你能够会遇到Go中内建类型error。Go语言使用error*值来显示异常状态。例如,os.Open在打开文件错误时,会返回一个非nil error值。github

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

下面的代码使用os.Open来打开一个文件。若是出现错误,会调用log.Fatal打印出错误的信息而且终止代码。golang

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类型的值来展现本身。下面是它的接口描述:json

type error interface {
    Error() String
}

error类型,就像其余内建类型同样,==是在全局中预先声明的==。这意味着咱们不用导入就能够在任何地方使用它。网络

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

// 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")。code

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

相关文章
相关标签/搜索