登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

being23

写给未来的自己

 
 
 
 
 

日志

 
 
关于我

真正的坚定,就是找到力量去做自己喜欢的事情,并为之努力,这样才会觉得生活是幸福的。

Go中的错误处理  

2013-10-03 00:33:44|  分类: work@oppo |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

Go的官方博客有很多的干货,这篇文章Error handling and Go说明了Go错误处理的一些小技巧。


Go中的错误处理

介绍

如果你折腾过一阵子Go语言,那么可能已经见到过了内置的error类型。Go语言中使用error类型来表示错误状态。例如,函数os.open在打开文件失败时,会返回一个non-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

在Go语言中,只要知道error的类型就能作很多事情了,但是在本文中会进一步介绍error,并讨论一些好的错误处理方式。

error 类型

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

type error interface {
    Error() string
}

error类型,像所有内置的类型,在universe block预先定义好了。

最常用到的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就会收到一个non-nil的error值(具体类型是errors.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一样格式化字符串,并将其作为一个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))
}

这样,有经验的调用方就可以通过type assertion来检测NegativeSqrtError并处理它,然而只是将这个错误传递给fmt.Println或者log.Fatal不会看到行为上的改变。

作为另一个例子,json包中当函数json.Decode在解析JSON blob遇到语法错误时会返回一个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,但是调用放可以用它来增加文件和行号到错误信息中:

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
}

(这是来自工程Camlistor一段代码的简化版本。)

error接口只要求Error方法;特殊的错误实现可能包含其他的方法。例如,包net会返回常见的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代码冗余,所幸的是可以通过某些方法最小化重复的出错处理。

考虑这样一个App Engine应用,它的HTTP处理器负责从datastore中获取一条记录然后根据模板格式化。

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.GetviewTemplateExecute方法返回的错误。在两种情形中,都会通过HTTP的状态码500(”Internal Server Error”)给用户返回一条简单的错误信息。这样的代码量看来还能接收,但是增加更多的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)最终调用函数fn

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

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

通过采用这种基本的错误处理结构,对用户来说变得更加友好。 除了简单的给出错误字符串,如果开发者在调试的错误日志中能看到一个简单的错误消息以及HTTP状态码就更好了。

要实现这个,我们声明了一个appError结构,包含一个error和其他域:

type appError struct {
    Error   error
    Message string
    Code    int
}

接下来,我们修改appHandler的类型让它返回 *appError

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

(通产来说,返回一个具体类型的错误而非一般的error是不对的,具体原因见the Go FAQ,但是在这里这样做是可以的,因为ServeHTTP是唯一看到这个错误并处理它的地方。)

修改appHandler方法ServeHTTP,使得用户能够获取appErrorMessage以及HTTP状态码,同时将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写一个constructor函数来存储栈信息从而简化调试
  • appHandler的panic中recover,将错误以Critical级别记录到控制台,同时告诉用户”a serious error has occurred.”。这有助于避免用户面对由于程序错误导致的不可预知错误。更多细节见 Defer, Panic, and Recover

结论

正确的错误处理是良好的软件所必须的。通过应用本文介绍的方法,可以写出更可靠的,简介的Go代码。

作者 Andrew Gerrand

2013-10-02@深圳 坪洲

  评论这张
 
阅读(1730)| 评论(0)

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018