会被defer里面包车型大巴recover函数捕获,3.error

2019-09-20 13:31栏目:大奖888官网登录
TAG:

error接口
1.error就是一个接口interface
2.属于errors包,该包有一个导出方法New,返回了errorString类型
3.errorString类型实现了error接口
4.之所以这样是因为可以实现每个错误都是不同的实例

背景介绍

如果你有写过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}// do something with the open *File f

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

error与panic

error:可预见的错误

panic:不可预见的异常

package mainimport ("errors""fmt""syscall")/*errors包中的代码package errors//定义了接口type error interface {    Error() string}//大写字母开头的方法,可以导出//返回了errorStrig类型func New(text string) error { return &errorString{text} }//定义类型type errorString struct { text string }//类型实现接口的方法func (e *errorString) Error() string { return e.text }*/func main() {//返回false,这俩是不相同的实例fmt.Println(errors.New("tsh error") == errors.New("tsh error"))//fmt.Errorf进行了包装fmt.Println(fmt.Errorf("我是 %s 错误", "tsh"))//类似实现了error接口var err error = syscall.Errnofmt.Println(err.Errorfmt.Println}

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 := Sqrtif err != nil { fmt.Println}

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 float64func (f NegativeSqrtError) Error() string { return fmt.Sprintf("math: square root of negative number %s", float64}

一个复杂的调用者就可以使用类型断言(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; 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 nettype 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 continue}if err != nil { log.Fatal}

panic处理

通过panic,defer,recover来处理异常

如下示例代码,当一个http链接到来时,golang会调用serve函数,serve函数会解析http协议,然后交给上层的handler处理,如果上层的handler内抛出异常的话,会被defer里面的recover函数捕获,打印出当前的堆栈信息,这样就可以防止一个http请求发生异常导致整个程序崩溃了。

func (c *conn)serve() {
    defer func() {
        if err := recover(); err != nil {
            const size = 64 << 10
            buf := make([]byte, size)
            buf = buf[:runtime.Stack(buf, false)]
            fmt.Printlf("http: panic serving %v: %vn%s", c.remoteAddr, err, buf)
        }
    }()
    w := c.readRequest()
    ServeHTTP(w, w.req)
} 

图片 1

简化捕获重复的错误

Go中,错误捕获是很重要的。Go的语言特性和使用习惯鼓励你在错误发生时做出明确的检测(这和那些抛出异常的然后有时捕获他们的语言有些区别)。在某些情况,这种方式会造成Go代码的冗余,不过幸运的是我们能使用一些技术来减少这种重复的捕获操作。

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

func init() { http.HandleFunc("/view", viewRecord)}func viewRecord(w http.ResponseWriter, r *http.Request) { c := appengin.NewContext key := datastore.NewKey(c, "Record", r.FormatValue, 0, nil) record := new if err := datastore.Get(c, key, record); err != nil { http.Error(w, err.Error return } if err := viewTemplate.Execute(w, record); err != nil { http.Error(w, err.Error } }

这个函数捕获从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 key := datastore.NewKey(c, "Record", r.FormValie, 0, nil) record := new 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; err != nil { http.Error(w, err.Error }}

ServeHTTP方法调用appHandler方法并且将返回的错误展示给用户。注意,ServeHTTP方法的接受者是一个函数。(go语言允许这样做)这个方法通过表达式fn来调用他的接受者,使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; e != nil { // e is *appError, not os.Error. c := appengine.NewContext 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 key := datastore.NewKey(c, "Record", r.FormValue, 0, nil) record := new 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这篇文章来获取更多的信息。

error处理

  

结论

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

Error handling and Go

Go by Example: Errors

在defer中集中处理错误

func demo() {
    var err error 
    defer func() {
        if err != nil {
            //  处理发生的错误,打印日志等信息
            return 
        }
    }
    err = func1()
    if err != nil {
        return 
    }
    err = func2()
    if err != nil {
        return 
    }
    .....
} 

all-or-nothing check

如果一个完整的任务内有多个小的任务,我们没有必要执行完每个小的任务都检查下是否有错,我们可以在所有的小任务执行完成后再去判断整个任务的执行过程中有没有错误。

//冗长的代码,满篇重复的 if err != nil 
_, err = fd.Write(p0[a:b])
if err != nil {
    return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
    return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
    return err
}
// and so on

我们可以这样简化代码

type errWriter struct {
    w   io.Writer
    err error
}
func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// 只需要在最后检查一次
if ew.err != nil {
    return ew.err
}

这种处理方式有一个明显的问题是不知道整个任务是在哪一个小的任务执行的时候发生错误,如果我们需要关注每个小任务的进度则这种方式就不适合了。

错误上下文

error是一个接口,任何实现了Error()函数的类型都满足该接口,errors.New()实际上返回的是一个errorString类型,该类型内仅包含一个string字符串,没有错误发生的上下文信息,当我们把error不断向上抛,在上层做统一处理时,只能输出一个字符串信息而不知道错误是在什么地方什么情况下发生的。

type error interface {
    Error() string
}
type errorString struct {
    s string
}
func (e *errorString) Error() string {
    return e.s
}
func New(text string) error {
    return &errorString{text}
}

既然error是一个接口,那我们也可以自己实现一个满足该接口的类型,并记录下发生错误的文件,函数,堆栈等信息,更方便错误的查找。

package stackerr
import (
    "fmt"
    "runtime"
    "strings"
)
type StackErr struct {
    Filename      string
    CallingMethod string
    Line          int
    ErrorMessage  string
    StackTrace    string
}
func New(err interface{}) *StackErr {
    var errMessage string
    switch t := err.(type) {
    case *StackErr:
        return t
    case string:
        errMessage = t
    case error:
        errMessage = t.Error()
    default:
        errMessage = fmt.Sprintf("%v", t)
    }
    stackErr := &StackErr{}
    stackErr.ErrorMessage = errMessage
    _, file, line, ok := runtime.Caller(1)
    if ok {
        stackErr.Line = line
        components := strings.Split(file, "/")
        stackErr.Filename = components[(len(components) - 1)]
    }
    const size = 1 << 12
    buf := make([]byte, size)
    n := runtime.Stack(buf, false)
    stackErr.StackTrace = string(buf[:n])
    return stackErr
}
func (this *StackErr) Error() string {
    return this.ErrorMessage
}
func (this *StackErr) Stack() string {
    return fmt.Sprintf("{%s:%d} %snStack Info:n %s", this.Filename, this.Line, this.ErrorMessage, this.StackTrace)
}
func (this *StackErr) Detail() string {
    return fmt.Sprintf("{%s:%d} %s", this.Filename, this.Line, this.ErrorMessage)
}

第三方错误库推荐

github.com/pkg/errors是一款提供了日志上下文封装的error库,可以非常方便的给error加上额外的信息、记录stack信息等等。详细使用说明可以参考github或者源码。

版权声明:本文由大奖888-www.88pt88.com-大奖888官网登录发布于大奖888官网登录,转载请注明出处:会被defer里面包车型大巴recover函数捕获,3.error