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 中打印相关信息可以统一在最外层中间件打印,而不要直接在获得错误的时候打印。这样就能避免多次打印重复的内容,这是代码规范的范畴。
2 中返回错误,则可以使用fmt.Errorf
层层包装更多的信息。
最简单的错误体系,是在包的开头用New
定义一堆基础错误,比如io/io.go
中有这些定义:
var errInvalidWrite = errors.New("invalid write result")
var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")
...
不要动态地定义错误,而应该使用%w
封装基础错误类型。因为动态地定义错误会让错误的判定变得复杂:
// bad
str, err := f.Read()
if err != nil {
return fmt.Errorf("getFile error: %v", err) // 上层无法简单地判定错误的类型
}
所有其它的错误利用fmt.Errorf
对基础错误进行层层封装:
str, err := f.Read() // 比如这个自定义的实现中会返回eof
if errors.Is(err, io.EOF) {
// 注意这个是%w,且只允许出现一次,返回到上层
return fmt.Errorf("getFile error: %w", err)
}
使用%w
封装错误,返回到上层的错误仍然可以使用errors.Is进行判定。
在这个体系中,错误要么是预定义的基础错误,要么是基础错误通过fmt.Errorf
的封装,十分简单。
用户可以:
在微服务中,返回一个错误码可以方便服务间的判定。
这时errors.New
定义的错误就不太够用了。需要定义一个结构实现Error
接口:
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
,检查是否存在错误并获得错误码:
// 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很多官方库的设计思路。