Golang errors 最佳实践 Part II
这个系列的文章有一定的摘抄,来自于原文,也包括译文等等。
技巧总是被不断重复,本系列的目的是整合 Golang 错误模型,所以只好抄一些、编一些。
系列被分为两个部分:
辅助库
hedzr/errors 是一个辅助库,它的用途是在兼容标准库的同时,涵盖 pkg/errors 的特性,同时提供更多(但仅提供必需品)的特性来帮助你简化错误开发模型。
hedzr/errors 同时也将 go 1.13 所提供的有关 errors 的新特性向下兼容到直至 go 1.11,你可以透明地运行这些新特性。
唯一的例外是 fmt.Errorf 中的 %w
特性
下面对 hedzr/errors 的主要内容进行介绍,但极少数内容或已过时,此时当然以 live codes 为准。
基本兼容性
hedzr/errors
复制了标准库(go 1.13+)的特性。这包括:
func As(err error, target interface{}) bool
func Is(err, target error) bool
func New(text string) error
func Unwrap(err error) error
此外,hedzr/errors
也复制了 pkg/errors
的特性,这包括(且不限于):
func Wrap(err error, message string) error
func Cause(err error) error
: unwraps recursively, just like Unwrap()func Cause1(err error) error
: unwraps just one levelfunc WithCause(cause error, message string, args ...interface{}) error
, =Wrap
- supports Stacktrace
- in an error by
Wrap()
, stacktrace wrapped; - for your error, attached by
WithStack(cause error)
;
- in an error by
在上面所作出的兼容性的努力,是为了让你能够无感知地平滑迁移到 hedzr/errors
。这样做的最大目的,还是为了让 hedzr/errors
所提供的增强特性能够以最低代价为你所使用。最低代价是简单到将所有 import 语句
1
2
import "errors"
import "pkg/errors"
替换为:
1
import "gopkg.in/hedzr/errors.v2"
就可以了。
现在你可以照原样编写代码:
使用 Is() 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
// Similar to:
// if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
// something wasn't found
}
// Similar to:
// if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// // query failed because of a permission problem
// }
if errors.Is(err, ErrPermission) {
// err, or some error that it wraps, is a permission problem
}
使用 As() 转换或抽出
1
2
3
4
5
6
// Similar to:
// if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
// err is a *QueryError, and e is set to the error's value
}
用%w包装错误并抽出它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if err != nil {
// asssumed err as a ErrPermission
// Return an error which unwraps to err.
return fmt.Errorf("decompress %v: %w", name, err)
}
if errors.Is(err, ErrPermission) {
// err, or some error that it wraps, is a permission problem
}
var e *ErrPermission
if errors.As(err, &e) {
// err is a *QueryError, and e is set to the error's value
}
e = errors.Unwrap(err)
if e == ErrPermission {
// ...
}
hedzr/errors
的增强特性
\1. New(msg, args...)
New(msg, args...)
统一了 New, Newf
(如果有这个名字的话), WithMessage, WithMessagef, …。仅需一个原型,就可以将上述的 errors, pkg/errors 中的附带信息的接口覆盖掉。
这样的后果是有轻微的性能损失,原因是
New(msg, args...)
会采用一个 if 测试来区别 New(msg) 和 New(msg, args…) 两种情况,这个条件测试是额外的损失。
1
2
var err = errors.New("hello error: %v", randNumber)
var err = errors.New("hello error: %w", innerError) // 支持,但不建议
在文本消息模版 msg
中采用 go 1.13 的 %w
是可行的,但并不推荐这么做。你应该使用间接方式,稍后我们还会进一步介绍(参见 关于 WithStackInfo):
1
var err = errors.New("tip mseesage").Attach(causeError)
或者是使用 errrors.Wrap()
,参见接下来的两小节。
hedzr/errors
的 New() 具有如下原型:
1
func New(message string, args ...interface{}) *WithStackInfo { ... }
请注意 WithStackInfo
是一个 error 对象,在后文中对其有一个介绍。
\2. WithCause(cause, msg, args...)
这是一个附加上内嵌错误 cause 以及文本信息的接口。其原型为:
1
func WithCause(cause error, message string, args ...interface{}) error
用法为:
1
var err = errors.WithCause(io.EOF, "hello %s", "world")
\3. Wrap(err, msg, args...) error
这是和 WithCause 等价的接口,但还额外提供上下文调用栈信息。
其原型为:
1
func Wrap(err error, message string, args ...interface{}) *WithStackInfo
用法为:
1
var err = errors.Wrap(io.EOF, "hello %s", "world")
\4. DumpStacksAsString()
这只是一个工具函数。它返回调用栈信息,如同 debug.PrintStack()
所做的那样。
其原型为:
1
func DumpStacksAsString(allRoutines bool) string
\5. CanXXX
:
通过 hedzr/errors
提供的 CanXXX 接口,你可以做一些特征性的测试。
CanAttach(err interface{}) bool
CanCause(err interface{}) bool
CanUnwrap(err interface{}) bool
CanIs(err interface{}) bool
CanAs(err interface{}) bool
关于 WithStackInfo
请注意 WithStackInfo
是一个实现了 error 接口的结构类。它实现了如下的全部接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// CauseInterface is an interface with Cause
type CauseInterface interface {
// Cause returns the underlying cause of the error, if possible.
// An error value has a cause if it implements the following
// interface:
//
// type causer interface {
// Cause() error
// }
//
// If the error does not implement Cause, the original error will
// be returned. If the error is nil, nil will be returned without further
// investigation.
Cause() error
// SetCause sets the underlying error manually if necessary.
SetCause(cause error) error
}
// FormatInterface is an interface with Format
type FormatInterface interface {
// Format formats the stack of Frames according to the fmt.Formatter interface.
//
// %s lists source files for each Frame in the stack
// %v lists the source file and line number for each Frame in the stack
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
// %+v Prints filename, function, and line number for each Frame in the stack.
Format(s fmt.State, verb rune)
}
// IsAsUnwrapInterface is an interface with Is, As, and Unwrap
type IsAsUnwrapInterface interface {
// Is reports whether any error in err's chain matches target.
Is(target error) bool
// As finds the first error in err's chain that matches target, and if so, sets
// target to that error value and returns true.
As(target interface{}) bool
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
Unwrap() error
}
// AttachInterface is an interface with Attach
type AttachInterface interface {
// Attach appends errs
Attach(errs ...error) *WithStackInfo
}
// ContainerInterface is an interface with IsEmpty
type ContainerInterface interface {
// IsEmpty tests has attached errors
IsEmpty() bool
}
// WithStackInfoInterface is an interface for WithStackInfo
type WithStackInfoInterface interface {
CauseInterface
FormatInterface
IsAsUnwrapInterface
AttachInterface
ContainerInterface
}
其它增强
错误容器
error Container and sub-errors (wrapped, attached or nested)
错误容器是可以容纳一系列多个子错误的容器,它有如下的关键接口:
NewContainer(message string, args ...interface{}) *withCauses
ContainerIsEmpty(container error) bool
AttachTo(container *withCauses, errs ...error)
withCauses.Attach(errs ...error)
我们曾提及标准库常常在结构中缓存一个 error 对象用以将过程中的错误延迟到业务结束时再行处理。标准库的做法是一旦有一个错误发生了,那么后续的交易一律放弃。
然而我们的业务也许是一个批量性的操作,一个子交易失败不必终止其它子交易的进行。在这种情况下我们可以用错误容器来代替单个的 error 对象缓存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type bizStrut struct {
err errors.Holder
w *bufio.Writer
}
func (bw *bizStrut) Write(b []byte) {
_, err := bw.w.Write(b)
bw.err.Attach(err)
}
func (bw *bizStrut) Flush() error {
err := bw.w.Flush()
bw.err.Attach(err)
return bw.err.Error()
}
func TestContainer2(t *testing.T) {
var bb bytes.Buffer
var bw = &bizStrut{
err: errors.NewContainer("bizStrut have errors"),
w: bufio.NewWriter(&bb),
}
bw.Write([]byte("hello "))
bw.Write([]byte("world "))
if err := bw.Flush(); err != nil {
t.Fatal(err)
}
}
你能看到,我们首先用 errors.NewContainer() 返回一个 errors.Holder
对象,并不断地将 err 压入这个 holder (bw.err
)中。在最后,我们通过 holder.Error() 将全部错误打包取出,这里面利用到了我们的 errors.WithCauses
结构体,这个结构体允许我们将一组 error 集合嵌入一个大的 error 容器中。
对于传入的 err==nil 的情况,实际上 holder 能够安全地忽略它,并不会接纳它。
同样地,对于循环操作一组子业务的情况,也可以直接编写代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func a() (err error){
container := errors.NewContainer("sample error")
// ...
for {
// ...
// in a long loop, we can add many sub-errors into container 'c'...
errors.AttachTo(container, io.EOF, io.ErrUnexpectedEOF, io.ErrShortBuffer, io.ErrShortWrite)
// Or:
// container.Attach(someFuncReturnsErr(xxx))
// ... break
}
// and we extract all of them as a single parent error object now.
err = container.Error()
return
}
func b(){
err := a()
// test the containered error 'err' if it hosted a sub-error `io.ErrShortWrite` or not.
if errors.Is(err, io.ErrShortWrite) {
panic(err)
}
}
尽管这段示例代码中采用了 errors.AttachTo(container, ...)
而不是 container.Attach(...)
,但两者并没有什么不同,喜欢用哪一种方式取决于你喜欢用什么样的视角来看待这段逻辑。
Coded error
hedzr/errors
中也提供一组预定义错误号,并且准许你扩展自己的错误号到这个体系中。
Code
is a generic type of error codes /Code
是一个通用性的错误号类型errors.WithCode(code, err, msg, args...)
can format an error object with error code, attached inner err, message or msg template, and stack info. / 可以用errors.WithCode(code, err, msg, args...)
来格式化一个带有错误号的、可以包含嵌入 error 对象的、可以带有信息文本的总的 error 对象。Code.New(msg, args...)
is likeWithCode
. /Code.New(msg, args...)
和 `WithCode 是相似的,但没有那么多参数。但你总是可以使用 Code.New(…).Attach(err,…) 的方式进一步追加信息。Code.Register(codeNameString)
declares the name string of an error code yourself. /Code.Register(codeNameString)
能够将你定制的错误号和一个描述文本相关联,并注册到系统体系中。EqualR(err, code)
: compareserr
withcode
使用错误号系统,通常是这样的顺序:
1
2
3
4
5
6
7
8
9
10
// using the pre-defined error code
err := InvalidArgument.New("wrong").Attach(io.ErrShortWrite)
// customizing the error code
const MyCode001 Code = 1001
// and register its name
MyCode001.Register("MyCode001")
// and use the error code
err := MyCode001.New("wrong 001: no config file")
你首先通过 const MyCode001 Code = 1001
自定义一个错误号,然后将其注册到系统体系中(通常是在一个 init() 函数中调用 MyCode001.Register("MyCode001")
)。
在需要这个错误号的位置,利用 MyCode001.New("wrong 001: no config file")
构造一个错误场所恰当的实例对象 err,然后像处理其它 error 实例对象那样使用 err。
Try it at: https://play.golang.org/p/Y2uThZHAvK1
Error Template: late-formatting the coded-error
使用 NewTemplate(tmpl)
可以基于错误号创建一个错误对象的字符串格式化模版,稍后在错误现场可以用于就地格式化。
1
2
3
4
5
6
var errTmpl1001 = BUG1001.NewTemplate("something is wrong, %v")
err4 := errTmpl1001.FormatNew("unsatisfied conditions").Attach(io.ShortBuffer)
fmt.Println(err4)
fmt.Printf("%+v\n", err4)
REF
🔚
留下评论