go error处理

背景介绍

如果你有写过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代码。

参考资料:

Error handling and Go

Go by Example: Errors

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容

  • 错误处理 Go语言的错误设计是通过返回值的方式来让调用者对错误进行处理,通常我们的处理是对error类型的返回值进...
    Carrism阅读 2,564评论 0 0
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,714评论 0 38
  • error code(错误代码)=0是操作成功完成。error code(错误代码)=1是功能错误。error c...
    Heikki_阅读 3,350评论 1 9
  • 断了几天没记录每日幸福事件,没记录不代表没有哟。 1、昨夜猫儿点评作业,给了我一个精选,这等于可以获得一枚优秀勋章...
    少校了悟阅读 246评论 3 2
  • 简书,粗浅的理解,简单的一封家书,一封情书,一封属于自己的人生感悟之书。 情不至所起,一往而深,不知不觉间的一次点...
    天地飞皇阅读 173评论 0 0