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

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

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

Go 的 errors 包

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

type error interface {    Error() string}

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

package errorsfunc 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   TestXXXX2022/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库返回一个errNoRowsfunc 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   TestErrWrap2022/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.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库则不倡议这样做。比方 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链中的底层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,咱们写个 demo 跑一下:

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 类型。
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库返回一个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,咱们写个 demo 跑一下:

// errors.WithStack 包装了 sqlError // sqlError 位于 error 链的底层,下层的 error 曾经不再是 sqlError 类型// 应用类型断言无奈判断出底层的 sqlError,而应用 errors.As() 函数能够判断出底层的 sqlErrortype 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 =====

总结

这篇文章咱们意识了 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 源码解读、错误处理的优化与最佳实际