乐趣区

关于go:Go-error-源码解读错误处理的优化与最佳实践

Go 语言自从诞生起,它的错误处理机制始终被喷出翔🙂。

没错,Go 语言在诞生初期的确简陋得不行,但在多个版本迭代以及各位前辈的摸索下还是找到了 Go 语言「错误处理」的最佳实际。

上面咱们深刻理解下 Go 的 error 包,并探讨如何让咱们的 Go 我的项目领有清新的错误处理。

Go 的 errors 包

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

type error interface {Error() string
}

与此同时,Go 的 errors 包实现了这个接口:调用 errors.New() 就会返回 error 接口 的实现类 errorString,通过源码咱们看到errorString 的底层就是一字符串,可真是 ” 省事 ” 啊🙃。

package errors

func New(text string) error {return &errorString{text}
}

type errorString struct {s string}

func (e *errorString) Error() string {return e.s}

errors.New()函数 返回的是 errorString 的指针类型,这样做的目标是为了避免字符串产生碰撞。

咱们能够做个小测试: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   TestXXXX
2022/03/25 22:05:40 error1 != error2

这种创立 error 的形式很常见,在 Go 源码以及三方包源码中大量呈现。

var EOF = errors.New("EOF")

var ErrUnexpectedEOF = errors.New("unexpected EOF")

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

然而很惋惜的是,Go 的 error 设计并不能满足所有场景。

Go error 的设计缺点

error 具备二义性

产生 error != nil 时不再意味着肯定产生了谬误,比方 io.Reader 返回 io.EOF 来告知调用者数据曾经读取结束,而这并不算是一个谬误。

在两个包之间创立了依赖

比方咱们应用了 io.EOF 来检查数据是否读取结束,那么代码里肯定会导入 io 包。

错误信息太薄弱

只有一个字符串表白谬误,过于薄弱。

改良 Go error

当初咱们晓得 error 底层其实就是一字符串,它很简洁,但反过来也意味着 ” 简陋 ”,无奈携带更多错误信息。

自定义谬误类型

所以程序员们决定本人封装一个 error 构造体,比方 Go 源码中的 os.PathError。

type PathError struct {
    Op   string
    Path string
    Err  error
}

封装 error 堆栈信息

将 error 封装后的确能表白更多的错误信息,然而它还有一个致命问题:没有堆栈信息。

比方这种日志,鬼晓得代码哪一行报了错,Debug 时几乎要命。

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

咱们能够应用 github.com/pkg/error 包 解决这个问题,这个包提供了 errors.withStack() 办法 将堆栈信息封装进 error:

func WithStack(err error) error {
    if err == nil {return nil}
    return &withStack{
        err,
        callers(),}
}

type withStack struct {
    error
    *stack
}

避免 error 被笼罩

下层 error 想附带更多日志信息时,往往会应用 fmt.Errorf()fmt.Errorf() 会创立一个新的 error 笼罩掉本来的 error 类型,咱们写一个 demo 测试一下:

var errNoRows = errors.New("no rows")

// 模拟 sql 库返回一个 errNoRows
func sqlExec() error {return errNoRows}

func serviceNoErrWrap() error {err := sqlExec()
    if err != nil {// fmt.Errorf() 吞掉了本来的 errNoRows 类型谬误。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   TestErrWrap
2022/03/26 17:19:43 ===== errType don't equal errNoRows =====

同样,应用 github.com/pkg/error 包errors.Wrap()函数 能够帮忙咱们为 error 增加自定义的文本信息。

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

github.com/pkg/error 包 内容很多,这里不开展聊了,前面独自讲。

到此为止,咱们深刻意识了 Go 的 error,当初咱们谈谈如何在大型项目中做好错误处理。

error 解决最佳实际

优先解决 error

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

只解决 error 一次

在 Go 中,每个 err 只应该被解决一次。如果一个函数返回了 err,那么咱们有两个抉择:

  1. 抉择一:立刻解决 err(包含记日志等行为),而后 return nil(把谬误吞掉)。

这个行为能够被认为是对 error 做降级解决,所以肯定要小心处理函数返回值。

  1. 抉择二:间接 return err,把 err 抛给调用者。

如果咱们违反了这个准则会导致什么结果?请看反例:

// 试想如果 writeAll 函数出错,会打印两遍日志
// 如果整个我的项目都这么做,最初会惊奇的发现咱们在处处打日志,我的项目中存在大量没有价值的垃圾日志
// unable to write:io.EOF
// could not write config:io.EOF

type 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 库 则不倡议这样做。比方 Go 的 sql 库 会返回 sql.ErrNoRows 这种预约义谬误,而后咱们的业务代码将其包装后 return。

不通明的错误处理

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

这种形式代码耦合小,不须要判断特定谬误类型,也就不须要导入相干包的依赖。

当然了,在这种状况下,只有咱们调用函数,就肯定跟着一组 if err != nil{},这也是大家常常吐槽 Go 我的项目if err != nil{} 满天飞的起因😂。

目前咱们只探讨在调用 Go 内置库和第三方库时产生的 error 的最佳解决实际,业务层面的错误处理是一个独自的话题,当前独自写一篇聊。

优化错误处理流程

Go 因为代码中有数的 if err != nil 被诟病,当初我教大家一个优化技巧:

咱们先看看 bufio.scan() 是如何简化 error 解决的:

// CountLines() 实现了 "读取内容的行数" 性能
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
}

// 利用 bufio.scan() 简化 error 的解决: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。

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 对象,简化对 error 的解决:

type Header struct {Key, Value string}

type Status struct {
    Code   int
    Reason string
}

// WriteResponse()函数实现了 "构建 HttpResponse" 性能
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
}

Go.1.13 版本 error 的新个性

Go 1.13 版本借鉴了github.com/pkg/error 包,大幅加强了 Golang 语言判断 error 类型的能力,这些函数平时还是用失去的,咱们深刻学习下:

errors.UnWrap()

// 与 errors.Wrap()行为相同
// 获取 err 链中的底层 err
func 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,咱们写个 demo 跑一下:

var errNoRows = errors.New("no rows")

// 模拟 sql 库返回一个 errNoRows
func 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   TestErrIs
2022/03/25 18:35:00 ===== errors.Is() succeeded =====

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

源码解读:

  • 外部调用了 err = Unwrap(err) 办法来获取 error 链中每一个 error。
  • 兼容自定义 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 能够实现 `Is 接口 ` 自定义 error 类型判断逻辑
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {return true}
        if err = Unwrap(err); err == nil {return false}
    }
}

上面咱们尝试应用 erros.Is() 辨认自定义 error 类型:

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 库返回一个 errNoRows
func 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   TestErrIs
2022/03/25 18:35:00 ===== errors.Is() succeeded =====

errors.As()

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

// errors.WithStack 包装了 sqlError 
// sqlError 位于 error 链的底层,下层的 error 曾经不再是 sqlError 类型
// 应用类型断言无奈判断出底层的 sqlError,而应用 errors.As() 函数能够判断出底层的 sqlError
type sqlError struct {error}

func (e *sqlError) IsNoRows() bool {t, ok := e.error.(ErrNoRows)
    return ok && t.IsNoRows()}

type ErrNoRows interface {IsNoRows() bool
}

// 返回一个 sqlError
func sqlExec() error {return sqlError{}
}

// errors.WithStack 包装 sqlError
func 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   TestErrAs
2022/03/25 18:09:02 ===== errors.As() succeeded =====

总结

这篇文章咱们意识了 Go 的 error,钻研了 error 包github.com/pkg/error 包 的源码,也聊了聊针对 Go 我的项目错误处理的优化与最佳实际,文中有大量 Demo 代码,倡议 copy 代码跑上两遍,有助于了解我薄弱的文字,有助于疾速把握 Go 的 error 解决。


参考:

  1. 《Effective GO》
  2. Go 程序设计语言》
  3. https://dave.cheney.net/practical-go/presentations/qcon-china.html#_error_handling

文章归档:Go 源码解读

转载申明:本文容许转载,原文地址:Go error 源码解读、错误处理的优化与最佳实际

退出移动版