带你熟悉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
javagopanicjavaexceptionerrorpanic
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 类型记录了文件和行号以展示发生了什么:

// 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的定义者来说, 这个问题就变得需要格外重视,我们需要
不透明的错误处理。它的优势在于:减少代码之间耦合,调用者只需关心成功还是失败,无需关心错误内部

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

说白了就是不通过err来判断各种情况,作为调用者只关心是成功还是失败(err是否为nil)

Assert errors for behaviour, not type

在少数情况下,只有这种二分的处理方法是不够(只有成功、失败两种状态)。

// 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
temporaryTemporary
Temporarytruetrue
err

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

3.Handing Error Gracefully 🌟

3.1 Try to eliminate errors handle

1)在不破坏程序正确性和可读性的前提下,尽量减少error处理

例一

BadGood
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

errorerror注释
error注释
fmt.ErrorfError() pkg/errors".Wrap
error注释
  1. 是否需要被调用者捕获、处理
func Open(filePath string) error {
  _, err := createfile(filePath)
  return fmt.Errorf(“createfile: %s“, err)
}
fmt.Errorfpkg/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注释
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

sum typeprotoBufferoneof
var int|error result

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

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

6.总结

最后,我来帮你梳理一下本文的重点,也就是目前需要掌握的

// TODO 图片

上图是分别在业务代码中和库文件、api代码中使用error的建议

panic

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

      f()
  }()
}

func (u *usecese) usecase1() error {

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

}

func (u *usecese) usecase2() error {
 name, err := u.repo.getUserName(uid)
 if err != nil {
     return errors.WithMessage(err, "其他附加信息")
 }

 // 其他逻辑
 return nil
}
func f() error {
 err := A()
 if errors.Is(err, io.EOF){
     return nil
 }

 // 其他逻辑
 return nil
}
func f() error {
 err := A()
 if errA := new(errorA) && errors.As(err, &errA){
     // ...
 }

 // 其他逻辑
 return nil
}

最近我也是在设计、重构公司微服务错误、日志相关的基础设施,如果有时间,我也会写一下实践的文章。

7.

  • 我们在数据库操作的时候,比如 dao 层中当遇到一个 sql.ErrNoRows 的时候,是否应该 Wrap 这个 error,抛给上层。为什么,应该怎么做请写出代码?
  • 为什么不允许处处使用 errors.Wrap ?
  • errors.wrap/ WithMessage 有何作用,为什么不用标准库的 fmt.Errorf("%w")

reference

本文由mdnice多平台发布