乐趣区

关于程序员:golang-错误处理

带你相熟 golang error 相干的常识.

0. 引言

在这篇文章中,我会带你相熟 golang error 相干的常识,大略内容如下:

  1. golang error 和其余语言的比照,益处与弊病
  2. golang 中各种类型的谬误以及举荐的谬误类型
  3. 如何处理错误
  4. 对 go1.13 的谬误包进行解析
  5. 对 golang error 相干常识进行总结、梳理
  6. 思考题

如果本文中有任何的谬误、或者你有一些好的想法都能够及时的提出,心愿咱们共同进步。

注:本文中的所有代码都在:https://github.com/driftingbo…

1.Error vs Exception

1.1 Error 实质

  • Error 实质上是一个接口
type error interface{Error() string
}
  • 常常应用 errors.New()来返回一个 error 对象

例如规范库中的 error 定义, 通过 bufio 前缀带上上下文信息

var (ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)
  • errors.New()是返回的 error 对象的指针

为了避免在 error 比拟时,因为 error 外部内容定义雷同导致两个不同类型的 error 误判相等

1.2 Error 和 Exception 的区别

各语言演进:

  • C: 个别传入指针,通过返回的 int 值判断胜利还是失败
  • C++: 无奈晓得抛出的什么异样,是否抛出异样(只能通过文档)
  • JAVA: 须要抛出异样则办法的所有者必须申明,调用者也必须解决。解决形式、轻重水平都由调用者辨别。
  • GO: 不引入 exception,采纳多参数返回,个别最初一个返回值都是 error

error or panic:
如果你写过 java 你会感觉 go 中的 panic 解决很像 java 中的抛 exception, 那咱们到底是应用error 还是panic?

在 Go 中 panic 会导致程序间接退出,是一个致命的谬误,如果应用 panic recover 进行解决的话,会存在很多问题

1)性能问题,频繁 panic recover 性能不好
2)容易导致程序异样退出,只有有一个中央没有解决到就会导致程序过程整个退出
3)不可控,一旦 panic 就将解决逻辑移交给了内部,咱们并不能预设内部包肯定会进行解决

什么时候应用 panic 呢?

对于真正意外的状况,那些示意不可复原的程序谬误,例如索引越界、不可复原的环境问题、栈溢出,咱们才应用 panic

应用 error 代替 exception 的益处

  • 简略
  • 思考失败不是胜利
  • 没有暗藏的控制流
  • error are value

2.Error Type 🌟

2.1 Sentinel Error

ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")

预约义的谬误,毛病:

  • 不灵便,调用方应用 == 去比拟谬误值是否相等;一旦呈现 fmt.Errorf 这种携带上下文信息的 error,会毁坏相等性查看
  • 成为你的公共 api; 比方 io.reader,io.copy 这类函数都须要去判断谬误类型是否是 io.eof, 但这并不是一个谬误。
  • 创立了两个包之间的依赖

2.2 Error Types

Error types 是实现了 error 接口的自定义类型。例如 MyError 类型记录了文件和行号以展现产生了什么:

os 包下的 error 类型:https://golang.org/src/os/err…

// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string { return e.Op + "" + e.Path +": " + e.Err.Error() }

func (e *PathError) Unwrap() error { return e.Err}

Error types 长处:

  • 携带更多的上下文

Error types 毛病:

  • 调用者要应用类型断言和类型 switch,就要让自定义的 error 变为 public。这种模型会导致和调用者产生强耦合,从而导致 API 变得软弱。
  • 共享 error values 许多雷同的问题。

2.3 Opaque Error

当开始应用 errors.Cause(err, sql.ErrNoRows)xerrors.Is(err, sql.ErrNoRows) 时,就意味着 sql.ErrNoRows 作为实现细节被裸露给外界了,它成了 API 的一部分。

如果只是利用库代码进行业务开发,包装后作判断的作法能够被了解和承受的。

而对于 API 的定义者来说,这个问题就变得须要分外器重,咱们须要
不通明的错误处理。它的劣势在于:缩小代码之间耦合,调用者只需关怀胜利还是失败,无需关怀谬误外部

// 只需返回谬误而不假如其内容
func fn()error{x, err := bar.Foo()
  if err != nil {return err}
  // to do something
}

说白了就是不通过 err 来判断各种状况,作为调用者只关怀是胜利还是失败(err 是否为 nil)

Assert errors for behaviour, not type

在多数状况下,只有这种二分的解决办法是不够(只有胜利、失败两种状态)。

比方 net 包中的操作:https://golang.org/src/net/ne…

// An Error represents a network error.
type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?}
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {// to do something}
type temporary interface {Temporary() bool
}
// 在不导入包的状况下能够间接应用 err 相干行为
func IsTemporary(err error) bool {te, ok := err.(temporary)
  return ok && te.Temporary()}

能够将任何谬误传递给 IsTemporary 以确定谬误是否能够重试。

如果谬误没有实现 temporary 接口; 也就是说,它没有 Temporary 办法,那么谬误不是长期的。

如果谬误的确实现了 Temporary,那么如果 true 返回true,调用者能够重试该操作。

这里的要害是,此逻辑能够在不导入定义谬误的包,或者间接晓得任何对于 err的根底类型的状况下实现 – 咱们只是对它的行为感兴趣。

我倡议,
尽量避免 Sentinel Error、error types,去应用 Opaque Error,至多在库文件、公共 api 中.


3.Handing Error Gracefully 🌟

3.1 Try to eliminate errors handle

1)在不毁坏程序正确性和可读性的前提下,尽量减少 error 解决

例一

Bad Good
func AuthRequest() error{<br/> if err:= auth();err!=nil{<br/> return err<br/>}<br/> return nil<br/>} func AuthRequest(r, *Request) error{<br/> return auth(r.User)<br/>}

例二

咱们的代码里常常会有大量的 if error != nil {...} 这样的代码,在一些非凡的场景是能够优化的

咱们先看一个令人解体的代码。

func parse(r io.Reader) (*Point, error) {
    var p Point
    if err := binary.Read(r, binary.BigEndian, &p.Longitude); err != nil {return nil, err}
    if err := binary.Read(r, binary.BigEndian, &p.Latitude); err != nil {return nil, err}
    if err := binary.Read(r, binary.BigEndian, &p.Distance); err != nil {return nil, err}
    if err := binary.Read(r, binary.BigEndian, &p.ElevationGain); err != nil {return nil, err}
    if err := binary.Read(r, binary.BigEndian, &p.ElevationLoss); err != nil {return nil, err}
}

咱们能够看到这段代码其实次要是在调用 binary.Read() 办法,咱们能够参考 bufio.Scanner 的操作

scanner := bufio.NewScanner(input)

for scanner.Scan() {token := scanner.Text()
    // process token
}

if err := scanner.Err(); err != nil {// process the error}

谬误会保留到 Scanner 中,只进行最初一次的判断。利用这个思路,优化代码如下

type Reader struct {
    r   io.Reader
    err error
}

func (r *Reader) read(data interface{}) {
    if r.err == nil {r.err = binary.Read(r.r, binary.BigEndian, data)
    }
}

func parse(input io.Reader) (*Point, error) {
    var p Point
    r := Reader{r: input}

    r.read(&p.Longitude)
    r.read(&p.Latitude)
    r.read(&p.Distance)
    r.read(&p.ElevationGain)
    r.read(&p.ElevationLoss)

    if r.err != nil {return nil, r.err}

    return &p, nil
}

然而这种优化场景局限,只能使用到同一个业务对象的一直操作,对多种业务对象多种性能还是得老老实实写if err != nil

3.2 Annotating errors

除了间接将谬误返回,咱们还经常会给谬误携带更多的上下文信息再返回,因为足够的信息能力让咱们疾速的解决呈现的问题。
下文会把给 error 增加上下文的操作叫做error 正文

目前 error 正文 的形式如下:

  • fmt.Errorf取得一个格式化的 error,能够携带分外的错误信息
  • 自定义 error,通过实现 Error() 办法
  • 打包谬误,通过 pkg/errors".Wrap

如何抉择适宜的 error 正文 形式?需思考如下一点:

  1. 是否须要被调用者捕捉、解决
  • 无需被调用者捕捉、解决;应用fmt.Errorf

    func Open(filePath string) error {_, err := createfile(filePath)
      return fmt.Errorf(“createfile: %s“, err)
    }
  • 须要被客户端捕捉、解决

应用 fmt.Errorf,会毁坏相等性查看,导致只能通过错误信息字符串来比拟谬误是否相等,非常不牢靠;
咱们能够自定义谬误类型,携带上下文的同时,给调用者提供牢靠的判断形式;然而,自定义谬误相当繁琐,咱们能够应用 pkg/errors 这个包简化操作。
如下:

  // file package
  var FileNotExsist =“file path not exsist: %s“func Open(filePath string) error {_, err := createfile(filePath)
    return errors.warp(err, "createfile fail:")
  }

这里简略介绍一下这个包 github.com/pkg/errors,应用它去打包上游信息简直曾经是一个 golang 中的规范做法了。

它的应用非常简单,如果咱们要新生成一个谬误,能够应用 New 函数, 生成的谬误,自带调用堆栈信息。

// fundamental is an error that has a message and a stack, but no caller.
type fundamental struct {
    msg string
    *stack
}

func New(message string) error {
    return &fundamental{
        msg:   message,
        stack: callers(),}
}

这里的 fundamental 对象也是实现了 golang 内建 interface error 的 Error 办法.

如果有一个现成的 error,咱们须要对他进行再次包装解决,这时候有三个函数能够抉择。

// 只附加新的信息, 个别用于在业务代码中替换 fmt.Errorf()
func WithMessage(err error, message string) error

// 只附加调用堆栈信息, 个别用于包装对第三方代码(规范库或第三方库)的调用。func WithStack(err error) error

// 同时附加堆栈和信息, 个别用于包装对第三方代码(规范库或第三方库)的调用。func Wrap(err error, message string) error

其实下面的包装,很相似于 Java 的异样包装,被包装的 error,就是这个谬误的根本原因。所以这个错误处理库为咱们提供了 Cause 函数让咱们能够取得最原始的 error 对象。

func Cause(err error) error {
    type causer interface {Cause() error
    }

    for err != nil {cause, ok := err.(causer)
        if !ok {break}
        err = cause.Cause()}
    return err
}

应用 for 循环始终找到最基本(最底层)的那个 error。

以上的谬误咱们都包装好了,也收集好了,那么怎么把他们外面存储的堆栈、谬误起因等这些信息打印进去呢?其实,这个错误处理库的谬误类型,都实现了 Formatter 接口,咱们能够通过 fmt.Printf 函数输入对应的错误信息。

  • %s,%v // 性能一样,输入错误信息,不蕴含堆栈
  • %q // 输入的错误信息带引号,不蕴含堆栈
  • %+v // 输入错误信息和堆栈

⚠️ 不要屡次包装谬误,堆栈信息会反复。

如果屡次应用 WithStack(err),会将 stack 打印多遍,err 信息可能十分长。能够人肉去 check 上层有没有应用 WithStack(err),如果上层用了下层就不必。但这样会减少心智累赘,容易出错。咱们能够在调用是应用一个 wrap 函数,判断一下是否曾经执行 WithStack(err)。然而 github.com/pkg/errors 自定义的 error 类型 withStack 是公有类型,如何去判断是否曾经执行 WithStack(err) 呢?好在 StackTrace 不是公有类型,所以咱们能够应用 interface 的一个小技巧,本人定义一个 interface,如果领有 StackTrace() 办法则不再执行 WithStack(err)。像这样:

type stackTracer interface {StackTrace() errors2.StackTrace
}

// once
func WithStack(err error) error {_, ok := err.(stackTracer)
    if ok {return err}

    return errors2.WithStack(err)
}

3.3 Only handle errors once

Handling an error means inspecting the error value, and making a decision.

我援用 dave.cheney 的这句话,意思是“处理错误意味着你曾经查看了谬误并做出了决定”.

如果你做出的决定 少于一个,那么就是你没有查看谬误、疏忽了谬误,如下:

func Write(w io.Writer, buf []byte) {w.Write(buf)
}

w.Write(buf)的 error 被抛弃了

如果针对一个问题做出的决定 多于一个,也是有问题的,如下:

func Write(w io.Writer, buf []byte) error {_, err := w.Write(buf)
        if err != nil {
                // annotated error goes to log file
                log.Println("unable to write:", err)
 
                // unannotated error returned to caller
                return err
        }
        return nil
}

在下面的例子中
咱们既记录谬误日志,又谬误返回给调用者,返回的谬误可能会被层层记录,始终到最上层。

最初咱们会失去一堆反复的日志信息,但在最上层却只能拿到一个最原始的谬误。

咱们能够依据不同需要去抉择error 正文(具体抉择形式查看 3.2),返回给调用者

func Write(w io.Write, buf []byte) error {_, err := w.Write(buf)
        return errors.Wrap(err, "write failed")
}

4.Golang1.13 error

联合社区反馈,Go 团队实现了在 Go 2 中简化错误处理的提案。Go 外围团队成员 Russ Cox 在 xerrors 中局部实现了提案中的内容。它用与 github.com/pkg/errors 类似的思路解决同一问题,引入了一个新的 fmt 格式化动词: %w,应用 Is 进行判断。:

import (
   "database/sql"
   "fmt"

   "golang.org/x/xerrors"
)

func bar() error {if err := foo(); err != nil {return xerrors.Errorf("bar failed: %w", foo())
   }
   return nil
}

func foo() error {return xerrors.Errorf("foo failed: %w", sql.ErrNoRows)
}

func main() {err := bar()
   if xerrors.Is(err, sql.ErrNoRows) {fmt.Printf("data not found, %v\n", err)
      fmt.Printf("%+v\n", err)
      return
   }
   if err != nil {// unknown error}
}
/* Outputs:data not found, bar failed: foo failed: sql: no rows in result set
bar failed:
    main.bar
        /usr/four/main.go:12
  - foo failed:
    main.foo
        /usr/four/main.go:18
  - sql: no rows in result set
*/

与 github.com/pkg/errors 相比,它有几点有余:

  • 应用 : %w 代替了 Wrap 看似简化,但失去了编译期查看。如果没有冒号,或 : %w 不位于于格式化字符串的结尾,或冒号与百分号之间没有空格,包装将生效且不报错
  • 更重大的是,调用 xerrors.Errorf 之前须要对参数进行 nil 判断。这理论齐全没有简化开发者的工作

到了 Go 1.13,xerrors 的局部性能被整合进了规范库。它继承了 xerrors 的全副毛病,并额定奉献了一项:不反对调用栈信息输入 .
依据官网的说法,此性能没有明确时间表。因而其实用性远低于 github.com/pkg/errors

因而目前没有应用它的必要


5.go 2 inspection

https://go.googlesource.com/p…

go2 中提案增加 sum type,即一个类型能够示意多个类型;有点相似protoBufferoneof类型;

var int|error result

那么这个 result 要么是 int,要么是 error 类型;

这也解决了 go 中没有泛型代理的局部问题;

6. 总结

最初,我来帮你梳理一下本文的重点,也就是目前须要把握的

// TODO 图片

上图是别离在业务代码中和库文件、api 代码中应用 error 的倡议

panic

  • 在程序启动的时候,如果有强依赖的服务呈现故障时 panic 退出
  • 在程序启动的时候,如果发现有配置显著不符合要求,能够 panic 退出(进攻编程)
  • 其余状况下只有不是不可复原的程序谬误,都不应该间接 panic 应该返回 error
  • 在程序入口处,例如 gin 中间件须要应用 recover 预防 panic 程序退出
  • 在程序中咱们应该防止应用家养的 goroutine
  • 如果是在申请中须要执行异步工作,应该应用异步 worker,音讯告诉的形式进行解决,防止申请量大时大量 goroutine 创立
  • 如果须要应用 goroutine 时,应该应用同一的 Go 函数进行创立,这个函数中会进行 recover,防止因为家养 goroutine panic 导致主过程退出

    func Go(f func()){go func(){defer func(){if err := recover(); err != nil {log.Printf("panic: %+v", err)
              }
          }()
    
          f()}()}

    error

  • 咱们在应用程序中应用 github.com/pkg/errors 解决利用谬误,留神在公共库当中,咱们个别不应用这个
  • error 应该是函数的最初一个返回值,当 error 不为 nil 时,函数的其余返回值是不可用的状态,不应该对其余返回值做任何期待
  • 错误处理的时候应该先判断谬误,if err != nil 呈现谬误及时返回,使代码是一条晦涩的直线,防止过多的嵌套
  • 在应用程序中呈现谬误时,应用 errors.New 或者 errors.Errorf 返回谬误

func (u *usecese) usecase1() error {

money := u.repo.getMoney(uid)
if money < 10 {errors.Errorf("用户余额有余, uid: %d, money: %d", uid, money)
}
// 其余逻辑
return nil

}

  1. 如果是调用应用程序的其余函数呈现谬误,请间接返回,如果须要携带信息,请应用 errors.WithMessage

    func (u *usecese) usecase2() error {name, err := u.repo.getUserName(uid)
     if err != nil {return errors.WithMessage(err, "其余附加信息")
     }
    
     // 其余逻辑
     return nil
    }
  2. 如果是调用其余库(规范库、企业公共库、开源第三方库等)获取到谬误时,请应用 errors.Wrap 增加堆栈信息
  3. 切记,不要每个中央都是用 errors.Wrap 只须要在谬误第一次呈现时进行 errors.Wrap 即可
  4. 依据场景进行判断是否须要将其余库的原始谬误吞掉,例如能够把 repository 层的数据库相干谬误吞掉,返回业务错误码,防止后续咱们宰割微服务或者更换 ORM 库时须要去批改下层代码
  5. 留神咱们在根底库,被大量引入的第三方库编写时个别不应用 errors.Wrap 防止堆栈信息反复
  6. 禁止每个出错的中央都打日志,只须要在过程的最开始的中央应用 %+v 进行对立打印,例如 http/rpc 服务的中间件
  7. 错误判断应用 errors.Is 进行比拟

    func f() error {err := A()
     if errors.Is(err, io.EOF){return nil}
    
     // 其余逻辑
     return nil
    }
  8. 谬误类型判断,应用 errors.As 进行赋值

    func f() error {err := A()
     if errA := new(errorA) && errors.As(err, &errA){// ...}
    
     // 其余逻辑
     return nil
    }
  9. 如何断定谬误的信息是否足够,想一想当你的代码呈现问题须要排查的时候你的错误信息是否能够帮忙你疾速的定位问题,例如咱们在申请中个别会输入参数信息,用于辅助判断谬误
  10. 对于业务谬误,举荐在一个对立的中央创立一个谬误字典,谬误字典外面应该蕴含谬误的 code,并且在日志中作为独立字段打印,不便做业务告警的判断,谬误必须有清晰的谬误文档
  11. 不须要返回,被疏忽的谬误必须输入日志信息
  12. 只应该被解决一次,输入谬误日志也算解决, 一旦确定函数 / 办法将处理错误,谬误就不再是谬误。如果函数 / 办法依然须要收回返回,则它不能返回谬误值。它应该只返回零 (比方 降级解决 中,你返回了降级数据,而后须要 return nil)。
  13. 对同一个类型的谬误,采纳雷同的模式,例如参数谬误,不要有的返回 404 有的返回 200
  14. 处理错误的时候,须要解决已调配的资源,应用 defer 进行清理,例如文件句柄
  15. 尽量避免Sentinel Errorerror types,去应用Opaque Error,至多在库文件、公共 api 中.

最近我也是在设计、重构公司微服务谬误、日志相干的基础设施,如果有工夫,我也会写一下实际的文章。

7.思考题

  • 咱们在数据库操作的时候,比方 dao 层中当遇到一个 sql.ErrNoRows 的时候,是否应该 Wrap 这个 error,抛给下层。为什么,应该怎么做请写出代码?
  • 为什么不容许处处应用 errors.Wrap ?
  • errors.wrap/ WithMessage 有何作用,为什么不必规范库的 fmt.Errorf(“%w”)

reference

https://blog.csdn.net/u012516…
https://zhuanlan.zhihu.com/p/…
https://github.com/xxjwxc/ube…

https://time.geekbang.org/col…

https://dave.cheney.net/2012/…
https://dave.cheney.net/2016/…
https://dave.cheney.net/2015/…
https://dave.cheney.net/2014/…
https://rauljordan.com/2020/0…
https://morsmachine.dk/error-…
https://blog.golang.org/error…
https://www.ardanlabs.com/blo…
https://www.ardanlabs.com/blo…
https://dave.cheney.net/2016/…
https://commandcenter.blogspo…
https://blog.golang.org/error…
https://dave.cheney.net/2016/…

https://www.ardanlabs.com/blo…
https://crawshaw.io/blog/xerrors
https://blog.golang.org/go1.1…
https://medium.com/gett-engin…
https://medium.com/gett-engin…

go2

本文由 mdnice 多平台公布

退出移动版