golang简单设计错误系统

2024-06-19 13:15:38 浏览数 (2)

go大量地使用错误,但错误系统一直饱受诟病,早期errors包中只有一个光秃秃的New方法,使得很多著名的项目如GRPC也只能使用偏门方法处理错误。

在1.13后,errors包中新增了 As/Is两个方法,同时,fmt.Errorf中可以使用 %w进行错误的封装,这使得搭建简单的错误系统方便起来。

代码语言:javascript复制
    // fmt.Errorf error %w 封装
    err1 := fmt.Errorf("find error:%w", ErrUnknown)
    // 封装后的错误仍然是 ErrUnknown
    gtest.Assert(errors.Is(err1, ErrUnknown), true)

如何处理错误

一般情况下,当调用函数返回错误,我们会:

  1. 打印相关的调试信息,如错误的string,行号,堆栈等
  2. 将错误返回至更上层,直至用户
  3. 如果是致命错误,则直接调用Fatal终止程序。

1 中打印相关信息可以统一在最外层中间件打印,而不要直接在获得错误的时候打印。这样就能避免多次打印重复的内容,这是代码规范的范畴。

2 中返回错误,则可以使用fmt.Errorf层层包装更多的信息。

直接定义大法

最简单的错误体系,是在包的开头用New定义一堆基础错误,比如io/io.go中有这些定义:

代码语言:javascript复制
var errInvalidWrite = errors.New("invalid write result")

var ErrShortBuffer = errors.New("short buffer")

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

不要动态地定义错误,而应该使用%w封装基础错误类型。因为动态地定义错误会让错误的判定变得复杂:

代码语言:javascript复制
// bad
str, err := f.Read()
if err != nil {
    return fmt.Errorf("getFile error: %v", err) // 上层无法简单地判定错误的类型
}

所有其它的错误利用fmt.Errorf对基础错误进行层层封装:

代码语言:javascript复制
str, err := f.Read() // 比如这个自定义的实现中会返回eof
if errors.Is(err, io.EOF) {
    // 注意这个是%w,且只允许出现一次,返回到上层
    return fmt.Errorf("getFile error: %w", err) 
}

使用%w封装错误,返回到上层的错误仍然可以使用errors.Is进行判定。

在这个体系中,错误要么是预定义的基础错误,要么是基础错误通过fmt.Errorf的封装,十分简单。

用户可以:

  1. 判定错误的基础类型(使用errors.Is)
  2. 获取层层附加的error message,通过 err.Error()

定义错误码

在微服务中,返回一个错误码可以方便服务间的判定。

这时errors.New定义的错误就不太够用了。需要定义一个结构实现Error接口:

代码语言:javascript复制
type BaseError struct {
    ErrStr string
    Code   int
}

func (e *BaseError) Error() string {
    return fmt.Sprintf("errorMsg:%s, code:%d", e.ErrStr, e.Code)
}


func FromError(err error) (code int, has bool) {
    if target := (&BaseError{}); errors.As(err, &target) {
        return target.Code, true
    }

    if err != nil {
        // return unknown code
        return 1, true
    }

    return -1, false
}

const (
    // 未知错误
    ErrCodeUnknown = iota   1 // 从1开始
    // 超时
    ErrCodeTimeOut
)

var ErrUnknown = &BaseError{
    ErrStr: "unknown",
    Code:   ErrCodeUnknown,
}

var ErrTimeOut = &BaseError{
    ErrStr: "timeout",
    Code:   ErrCodeTimeOut,
}
......

FromError(err error) (code int, has bool) 类似grpc的status.FromError,检查是否存在错误并获得错误码:

代码语言:javascript复制
// fmt.Errorf error %w 封装
err1 := fmt.Errorf("find error:%w", ErrUnknown)
// 封装后的错误仍然是这个类型
gtest.Assert(errors.Is(err1, ErrUnknown), true)

// As 用法
if code, hasErr := FromError(err1); hasErr {
    t.Logf("target code:%v", code)
    gtest.Assert(code, 1)
}

在这个体系中,错误要么是预定义的BaseError,要么是BaseError通过fmt.Errorf的封装

并且可获取到最初始定义的错误码,方便服务间的错误处理。

到这里,这个错误系统已经能满足大部分的使用场景,且保持了简单。简单的东西不容易出错且易在团队中推广和使用,这也是go很多官方库的设计思路。

0 人点赞