golang简单设计错误系统 go大量地使用错误,但错误系统一直饱受诟病,早期errors包中只有一个光秃秃的New方法,使得很多著名的项目如GRPC也只能使用偏门方法处理错误。 在1.13后,errors包中新增了 `As/Is`两个方法,同时,fmt.Errorf中可以使用 %w进行错误的封装,这使得搭建简单的错误系统方便起来。 ``` // 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`中有这些定义: ```go var errInvalidWrite = errors.New("invalid write result") var ErrShortBuffer = errors.New("short buffer") var EOF = errors.New("EOF") ... ``` 不要动态地定义错误,而应该使用`%w`封装基础错误类型。因为动态地定义错误会让错误的判定变得复杂: ```go // bad str, err := f.Read() if err != nil { return fmt.Errorf("getFile error: %v", err) // 上层无法简单地判定错误的类型 } ``` 所有其它的错误利用`fmt.Errorf`对基础错误进行层层封装: ```go 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`接口: ```go 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`,检查是否存在错误并获得错误码: ```go // 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很多官方库的设计思路。 来自 大脸猪 写于 2024-06-18 14:39 -- 更新于2024-06-18 14:45 -- 0 条评论