error type: 错误定义与判断
Sentinel Error
哨兵错误,就是定义一些包级别的错误变量,然后在调用的时候外部包可以直接对比变量进行判定,在标准库当中大量的使用了这种方式。例如下方 io
库中定义的错误。
// EOF is the error returned by Read when no more input is available.
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF")
// ErrUnexpectedEOF means that EOF was encountered in the
// middle of reading a fixed-size block or data structure.
var ErrUnexpectedEOF = errors.New("unexpected EOF")
// ErrNoProgress is returned by some clients of an io.Reader when
// many calls to Read have failed to return any data or error,
// usually the sign of a broken io.Reader implementation.
var ErrNoProgress = errors.New("multiple Read calls return no data or error")
- 并不是所有error都表示错误,例如:
io.EOF
,可以理解为一个标识,代表数据读取完毕。
我们在外部判定的时候一般使用等值判定或者使用 errors.Is
进行判断。
if err == io.EOF {
//...
}
if errors.Is(err, io.EOF){
//...
}
- 会导致包与包之间存在依赖,阻碍重构,并且携带信息有限。
- 等值判断处理不了errwrap的情况,只能通过error.Error 的输出内容进行匹配。但是错误处理不能依赖 error.Error 的输出内容,这是给人看的,不是给程序看的。
- 应比较原始错误,可通过errors.Cause()获取。
- 有人指出
Sentinel Error
可以在test中使用,但是Dave
认为这是一种code smell
,应该避免(可能会泛滥)。
结论
不建议使用,或者至少不能用于公共API。
error types
这个就类似我们前面定义的 errorString
一样实现了 error
的接口,然后在外部是否类型断言来判断是否是这种错误类型
type MyStruct struct {
s string
name string
path string
}
// 使用的时候
func f() {
switch err.(type) {
case *MyStruct:
// ...
case others:
// ...
}
}
这种方式相对于哨兵来说,可以包含更加丰富的信息,但是同样也将错误的类型暴露给了外部,例如标准库中的 os.PathError
。
结论
不建议使用,或者至少不能用于公共API。
Opaque errors
不透明的错误处理,这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。虽然调用者知道发生了错误,但调用者没有能力看到错误的内部。作为调用者,关于操作的结果,只需指定成功还是失败。这就是不透明错误处理的全部功能–只需返回错误而不假设其内容。
- 被调用者可随意向error增添更多的信息,而不会影响调用者处理逻辑。
在少数情况下,这种二分错误处理方法是不够的。例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理。在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
-
对错误的判断封装到底层,通过
-
判断接口和error解耦,不同的error自行实现接口,error甚至可以和判断接口定义在不同的包中,达到error行为和error本身的解耦,很巧妙。
-
这种方式最大的特点就是只返回错误,暴露错误判定接口,不返回类型,这样可以减少 API 的暴露,后续的处理会比较灵活,这个一般用在公共库会比较好。
-
被调用者可随意向error增添更多的信息,而不会影响调用者处理逻辑。
结论
推荐使用opaque error,实在需要细化判断error,也只能判断他的行为,而不是类型和值。判断行为时使用github.com/pkg/errors/errors.go#Cause()
获取原始错误。
Go 1.13 errors
errors.Is()
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// TODO: consider supporting target.Is(err). This would allow
// user-definable predicates, but also may allow for coping with sloppy
// APIs, thereby making it easier to get away with them.
if err = Unwrap(err); err == nil {
return false
}
}
}
-
reflectlite.TypeOf(target).Comparable()
:先判断是否可通过==
比较-
Comparable()
:判断equal属性是否为空,equal是一个方法。func (t *rtype) Comparable() bool { return t.equal != nil } type rtype struct { size uintptr ptrdata uintptr // number of bytes in the type that can contain pointers hash uint32 // hash of type; avoids computation in hash tables tflag tflag // extra type information flags align uint8 // alignment of variable with this type fieldAlign uint8 // alignment of struct field with this type kind uint8 // enumeration for C // function for comparing objects of this type // (ptr to object A, ptr to object B) -> ==? equal func(unsafe.Pointer, unsafe.Pointer) bool gcdata *byte // garbage collection data str nameOff // string form ptrToThis typeOff // type for pointer to this type, may be zero }
-
指针变量默认比较两个指针内存地址。
-
仅包含string字段的结构体变量,equal为
runtime.strequal
《go语言精进之路》里string章节详细讲了string比较,后面会分享笔记。
-
包含多种类型的结构体变量,equal为
type..eq.main.MyError
type MyError struct { a int msg string }
结构体变量比较:结构体字段逐个比较。推测这里的的equal表示这个意思。
-
map、slience 不支持
==
比较,可以使用反射包里的reflect.DeepEqual
,不过性能差一些。如果是单元测试,可使用google/go-cmp。无法比较时会panic,故非测试代码使用时要注意。吐槽下go的
==
比较,是运行时go根据具体类型做不同的操作,没有在源码中体现。而java的比较更清晰,例如字符串比较,可以看到string源码重写的equals方法。
-
-
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target)
:断言是否实现Is(error) bool
接口,实现就直接调用。 -
err = Unwrap(err)
:递归解开error。有时需要通过err传递更多信息,就会对error进行包裹。func Unwrap(err error) error { u, ok := err.(interface { Unwrap() error }) if !ok { return nil } return u.Unwrap() }
errors.As()
errors.As(a,b)
:通过反射判断a是否和b是一个类型,如果是就把b赋值给a,如果不是,unwrap a,重复前面的步骤。
wrap error
errors/wrap.go
前一篇文章分析了error标准库里的error.Is(), error.As()
,如果要使用这套逻辑来wrap error,以达到携带更多上下文信息的目的。在做error判断时,需要自行实现Unwrap接口,才能解开已经wrap的error。想想还是挺复杂的,要自定义error,每个自定义error还得去实现Unwrap接口。
github.com/pkg/errors/errors.go
- 下面的As, Is, Unwrap是直接调用标准库的代码,kratos也是这么干的,推测这样做的好处是:需要处理错误时,只需导入
github.com/pkg/errors
包即可,而不用再导入同包名的标准库error包(需要做别名处理),不再纠结导入哪个error包。
github.com/pkg/errors
使用起来很方便,可以解决上诉问题。关键代码是Wrap(), Cause()
。前者用来wrap error,可携带额外信息和堆栈。后者用来解开已经wrap的error,得到最原始的error。
- 底层包不应wrap error,应该返回原始错误。
- 得到原始错误时,第一时间wrap,保留堆栈。
- 可为error增加更多的上下文信息,例如入参。
VS标准库的 fmt.Errorf("%w")
我们先看一下标准库的源码,我们可以发现当 p.wrappedErr != nil
的时候(也就是有 %w)的时候,会使用一个 wrapError
将错误包装,看 wrapError
的源码可以发现,这个方法只是包装了一下原始错误,并且可以做到附加一些文本信息,但是没有堆栈信息。
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
Copy
在看一下 pkg/errors 的源码,我肯可以发现除了使用 withMessage
附加了错误信息之外还使用 withStack
附加了堆栈信息,这样我们在程序入口处打印日志信息的时候就可以将堆栈信息一并打出了
// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}
handle error gracefully
封装error到对象属性,简化主干代码
bufio.scan
对比下面两个函数的处理我们可以发现, count2
使用 sc.Scan
之后一个 if err
的判断都没有,极大的简化了代码,这是因为在 sc.Scan
做了很多处理,像很多类似的,需要循环读取的都可以考虑像这样包装之后进行处理,这样外部包调用的时候就会非常简洁。
// 统计文件行数
func count(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
)
for {
// 读取到换行符就说明是一行
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
}
// 当错误是 EOF 的时候说明文件读取完毕了
if err != io.EOF {
return 0, err
}
return lines, err
}
func count2(r io.Reader) (int, error) {
var (
sc = bufio.NewScanner(r)
lines int
)
for sc.Scan() {
lines++
}
return lines, sc.Err()
}
- 这里将error封装到底层
sc.Err()
:将error作为字段,链式调用时很有用,gorm就是这么干的。
error writer
看一个来自 go blog 的例子:https://blog.golang.org/errors-are-values
一般代码
_, err = fd.Write(p0[a:b])
if err != nil {
return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
return err
}
// and so on
errWriter
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err == nil {
_, ew.err = ew.w.Write(buf)
}
}
// 使用时
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}
- 如果去翻 标准库中 bufio.Writer 的源代码,你会发现也有这种用法,这种就是将重复的逻辑进行了封装,然后把 error 暂存,然后我们就只需要在最后判断一下 error 就行了。
- 多次调用同一方法,并需要判断error的场景下适用。