Golang中的error

Golang中的 error 就是一个简略的接口类型。只有实现了这个接口,就能够将其视为一种 error

type error interface {    Error() string}

error的几种玩法

翻看Golang源码,能看到许多相似于上面的这两种error类型

哨兵谬误

var EOF = errors.New("EOF")var ErrUnexpectedEOF = errors.New("unexpected EOF")var ErrNoProgress = errors.New("multiple Read calls return no data or error")

毛病:
1.让 error 具备二义性

error != nil不再意味着肯定产生了谬误
比方io.Reader返回io.EOF来告知调用者没有更多数据了,然而这又不是一个谬误

2.在两个包之间创立了依赖

如果你应用了io.EOF来查看是否read完所有的数据,那么代码里肯定会导入io包

自定义谬误类型

一个不错的例子是os.PathError,它的长处是能够附带更多的上下文信息

type PathError struct {    Op   string    Path string    Err  error}

Wrap error

到这里咱们能够发现,Golang 的 error 非常简单,然而简略也意味着有时候是不够用的
Golang的error始终有两个问题:
1.error没有附带file:line信息(也就是没有堆栈信息)
比方这种error,鬼晓得代码哪一行报了错,Debug时几乎要命

SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!       Error 1406: Data too long for column 'content' at row 1

2.下层error想附带更多日志信息时,往往会应用fmt.Errorf()fmt.Errorf()会创立一个新的error,底层的error类型就被“吞”掉了

var errNoRows = errors.New("no rows")// 模拟sql库返回一个errNoRowsfunc sqlExec() error {    return errNoRows}func serviceNoErrWrap() error {    err := sqlExec()    if err != nil {        return fmt.Errorf("sqlExec failed.Err:%v", err)    }        return nil}func TestErrWrap(t *testing.T) {    // 应用fmt.Errorf创立了一个新的err,失落了底层err    err := serviceNoErrWrap()    if err != errNoRows {        log.Println("===== errType don't equal errNoRows =====")    }}-------------------------------代码运行后果----------------------------------=== RUN   TestErrWrap2022/03/26 17:19:43 ===== errType don't equal errNoRows =====

为了解决这个问题,咱们能够应用github.com/pkg/error包,应用errors.withStack()办法将err保
存到withStack对象

// withStack构造体保留了error,造成了一条error链。同时*stack字段保留了堆栈信息。type withStack struct {    error    *stack}

也能够应用errors.Wrap(err, "自定义文本"),额定附带一些自定义的文本信息
源码解读:先将err和message包进withMessage对象,再将withMessage对象和堆栈信息包进withStack对象

func Wrap(err error, message string) error {    if err == nil {        return nil    }    err = &withMessage{        cause: err,        msg:   message,    }    return &withStack{        err,        callers(),    }}

Golang1.13版本error的新个性

Golang1.13版本借鉴了github.com/pkg/error包,新增了如下函数,大大加强了 Golang 语言判断 error 类型的能力

errors.UnWrap()

// 与errors.Wrap()行为相同// 获取err链中的底层errfunc Unwrap(err error) error {    u, ok := err.(interface {        Unwrap() error    })    if !ok {        return nil    }    return u.Unwrap()}

errors.Is()

在1.13版本之前,咱们能够用err == targetErr判断err类型
errors.Is()是其增强版:error 链上的任一err == targetErr,即return true

// 实际:学习应用errors.Is()var errNoRows = errors.New("no rows")// 模拟sql库返回一个errNoRowsfunc sqlExec() error {    return errNoRows}func service() error {    err := sqlExec()    if err != nil {        return errors.WithStack(err)    // 包装errNoRows    }        return nil}func TestErrIs(t *testing.T) {    err := service()        // errors.Is递归调用errors.UnWrap,命中err链上的任意err即返回true    if errors.Is(err, errNoRows) {        log.Println("===== errors.Is() succeeded =====")    }        //err经errors.WithStack包装,不能通过 == 判断err类型    if err == errNoRows {        log.Println("err == errNoRows")    }}-------------------------------代码运行后果----------------------------------=== RUN   TestErrIs2022/03/25 18:35:00 ===== errors.Is() succeeded =====

例子解读:
因为应用errors.WithStack包装了sqlErrorsqlError位于error链的底层,下层的error曾经不再是sqlError类型,所以应用==无奈判断出底层的sqlError

源码解读:

  • 咱们很容易想到其外部调用了err = Unwrap(err)办法来获取error链中底层的error
  • 自定义error类型能够实现Is接口来自定义error类型判断办法

    func Is(err, target error) bool {  if target == nil {      return err == target  }    isComparable := reflectlite.TypeOf(target).Comparable()  for {      if isComparable && err == target {          return true      }      // 反对自定义error类型判断      if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {          return true      }      if err = Unwrap(err); err == nil {          return false      }  }}

上面咱们来看看如何自定义error类型判断:
自定义的errNoRows类型,必须实现Is接口,能力应用erros.Is()进行类型判断

type errNoRows struct {    Desc string}func (e errNoRows) Unwrap() error { return e }func (e errNoRows) Error() string { return e.Desc }func (e errNoRows) Is(err error) bool {    return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()}// 模拟sql库返回一个errNoRowsfunc sqlExec() error {    return &errNoRows{"Kaolengmian NB"}}func service() error {    err := sqlExec()    if err != nil {        return errors.WithStack(err)    }        return nil}func serviceNoErrWrap() error {    err := sqlExec()    if err != nil {        return fmt.Errorf("sqlExec failed.Err:%v", err)    }        return nil}func TestErrIs(t *testing.T) {    err := service()        if errors.Is(err, errNoRows{}) {        log.Println("===== errors.Is() succeeded =====")    }}-------------------------------代码运行后果----------------------------------=== RUN   TestErrIs2022/03/25 18:35:00 ===== errors.Is() succeeded =====

errors.As()

在1.13版本之前,咱们能够用if _,ok := err.(targetErr)判断err类型
errors.As()是其增强版:error 链上的任一err与targetErr类型雷同,即return true

// 通过例子学习应用errors.As()type sqlError struct {    error}func (e *sqlError) IsNoRows() bool {    t, ok := e.error.(ErrNoRows)    return ok && t.IsNoRows()}type ErrNoRows interface {    IsNoRows() bool}// 返回一个sqlErrorfunc sqlExec() error {    return sqlError{}}// errors.WithStack包装sqlErrorfunc service() error {    err := sqlExec()    if err != nil {        return errors.WithStack(err)    }        return nil}func TestErrAs(t *testing.T) {    err := service()        // 递归应用errors.UnWrap,只有Err链上有一种Err满足类型断言,即返回true    sr := &sqlError{}    if errors.As(err, sr) {        log.Println("===== errors.As() succeeded =====")    }        // 经errors.WithStack包装后,不能通过类型断言将以后Err转换成底层Err    if _, ok := err.(sqlError); ok {        log.Println("===== type assert succeeded =====")    }}----------------------------------代码运行后果--------------------------------------------=== RUN   TestErrAs2022/03/25 18:09:02 ===== errors.As() succeeded =====

例子解读:
因为应用errors.WithStack包装了sqlErrorsqlError位于error链的底层,下层的error曾经不再是sqlError类型,所以应用类型断言无奈判断出底层的sqlError

error解决最佳实际

下面讲了如何定义error类型,如何比拟error类型,当初咱们谈谈如何在大型项目中做好error解决

优先解决error

当一个函数返回一个非空error时,应该优先解决error,疏忽它的其余返回值

只解决error一次

在Golang中,对于每个err,咱们应该只解决一次。

  • 要么立刻解决err(包含记日志等行为),return nil(把谬误吞掉)。此时因为把谬误做了降级,肯定要小心处理函数返回值。

    比方上面例子json.Marshal(conf)没有return err ,那么在应用buf时肯定要小心空指针等谬误
  • 要么return err,在下层解决err
    反例:

    // 试想如果writeAll函数出错,会打印两遍日志// 如果整个我的项目都这么做,最初会惊奇的发现咱们在处处打日志,我的项目中存在大量没有价值的垃圾日志// unable to write:io.EOF// could not write config:io.EOFtype config struct {}func writeAll(w io.Writer, buf []byte) error {  _, err := w.Write(buf)  if err != nil {      log.Println("unable to write:", err)      return err  }    return nil}func writeConfig(w io.Writer, conf *config) error {  buf, err := json.Marshal(conf)  if err != nil {      log.Printf("could not marshal config:%v", err)  }    if err := writeAll(w, buf); err != nil {      log.Println("count not write config: %v", err)      return err  }    return nil}

    不要重复包装error

    咱们应该包装error,但只包装一次
    下层业务代码倡议Wrap error,然而底层根底Kit库不倡议
    如果底层根底 Kit 库包装了一次,下层业务代码又包装了一次,就反复包装了 error,日志就会打重
    比方咱们罕用的sql库会返回sql.ErrNoRows这种预约义谬误,而不是给咱们一个包装过的 error

    不通明的错误处理

    在大型项目中,举荐应用不通明的错误处理(Opaque errors):不关怀谬误类型,只关怀error是否为nil
    益处:

  • 耦合小,不须要判断特定谬误类型,就不须要导入相干包的依赖。

    不过有时候,这种解决error的形式不够用,比方:业务须要对参数异样error类型做降级解决,打印Warn级别的日志

    type ParamInvalidError struct {  Desc string}func (e ParamInvalidError) Unwrap() error { return e }func (e ParamInvalidError) Error() string { return "ParamInvalidError: " + e.Desc }func (e ParamInvalidError) Is(err error) bool {  return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()}func NewParamInvalidErr(desc string) error {  return errors.WithStack(&ParamInvalidError{Desc: desc})}------------------------------顶层打印日志---------------------------------if errors.Is(err, Err.ParamInvalidError{}) {  logger.Warnf(ctx, "%s", err.Error())  return}if err != nil {  logger.Errorf(ctx, " error:%+v", err)}

    简化错误处理

    Golang因为代码中有数的if err != nil被诟病,当初咱们看看如何缩小if err != nil这种代码

    bufio.scan

    CountLines() 实现了"读取内容的行数"性能
    能够利用 bufio.scan() 简化 error 的解决:

    func CountLines(r io.Reader) (int, error) {  var (      br    = bufio.NewReader(r)      lines int      err   error  )    for {      _, err := br.ReadString('\n')      lines++      if err != nil {          break      }  }    if err != io.EOF {      return 0, nilsadwawa   }    return lines, nil}func CountLinesGracefulErr(r io.Reader) (int, error) {  sc := bufio.NewScanner(r)    lines := 0  for sc.Scan() {      lines++  }    return lines, sc.Err()}

bufio.NewScanner() 返回一个 Scanner 对象,构造体外部蕴含了 error 类型,调用Err()办法即可返回封装好的error

Golang源代码中蕴含着大量的优良设计思维,咱们在浏览源码时从中学习,并在实践中得以使用
type Scanner struct {    r            io.Reader // The reader provided by the client.    split        SplitFunc // The function to split the tokens.    maxTokenSize int       // Maximum size of a token; modified by tests.    token        []byte    // Last token returned by split.    buf          []byte    // Buffer used as argument to split.    start        int       // First non-processed byte in buf.    end          int       // End of data in buf.    err          error     // Sticky error.    empties      int       // Count of successive empty tokens.    scanCalled   bool      // Scan has been called; buffer is in use.    done         bool      // Scan has finished.}func (s *Scanner) Err() error {    if s.err == io.EOF {        return nil    }    return s.err}

errWriter

WriteResponse()函数实现了"构建HttpResponse"性能
利用下面学到的思路,咱们能够本人实现一个errWriter对象,简化对 error 的解决

type Header struct {    Key, Value string}type Status struct {    Code   int    Reason string}func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {    _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)    if err != nil {        return err    }        for _, h := range headers {        _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)        if err != nil {            return err        }    }        if _, err := fmt.Fprintf(w, "\r\n"); err != nil {        return err    }        _, err = io.Copy(w, body)    return err}type errWriter struct {    io.Writer    err error}func (e *errWriter) Write(buf []byte) (n int, err error) {    if e.err != nil {        return 0, e.err    }        n, e.err = e.Writer.Write(buf)        return n, nil}func WriteResponseGracefulErr(w io.Writer, st Status, headers []Header, body io.Reader) error {    ew := &errWriter{w, nil}        fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)        for _, h := range headers {        fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)    }        fmt.Fprintf(w, "\r\n")        io.Copy(ew, body)        return ew.err}

何时该用panic

在 Golang 中panic会导致程序间接退出,是一个致命的谬误。
倡议产生致命的程序谬误时才应用 panic,例如索引越界、不可复原的环境问题、栈溢出等等

小补充

errors.New()返回的是errorString对象的指针,其起因是避免字符串产生碰撞,如果产生碰撞,两个 error 对象会相等。
源码:

func New(text string) error {    return &errorString{text}}// errorString is a trivial implementation of error.type errorString struct {    s string}func (e *errorString) Error() string {    return e.s}

实际:error1error2的text都是"error",然而二者并不相等

func TestErrString(t *testing.T) {    var error1 = errors.New("error")    var error2 = errors.New("error")        if error1 != error2 {        log.Println("error1 != error2")    }}---------------------代码运行后果--------------------------=== RUN   TestXXXX2022/03/25 22:05:40 error1 != error2

创作不易,大家棘手点个赞~这对我很重要,蟹蟹各位啦~

参考文献
《Effective GO》
《Go程序设计语言》
https://dave.cheney.net/practical-go/presentations/qcon-china.html#_error_handling