1
|
go get code.gopub.tech/errors
|
Show me the code:
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
|
import (
"testing"
"code.gopub.tech/errors"
)
func TestPrint(t *testing.T) {
err1 := errors.New("err1")
err2 := errors.New("err2")
err := errors.Errorf("prefix: %w, %w", err1, err2)
fmt.Printf("%+v\n", err)
}
/* Output 输出:
prefix: err1, err2
(1) attached stack trace
│ -- stack trace:
│ example_test.TestPrint
│ /path/to/new_errors_test.go:65
│ [...repeated from below...]
Next: (2) prefix: err1, err2
Next: (3) err1
│ err2
├─ Wraps: (4) attached stack trace
│ │ -- stack trace:
│ │ example_test.TestPrint
│ │ /path/to/new_errors_test.go:63
│ │ [...repeated from below...]
│ Next: (5) err1
└─ Wraps: (6) attached stack trace
│ -- stack trace:
│ example_test.TestPrint
│ /path/to/new_errors_test.go:64
│ testing.tRunner
│ /usr/local/go/src/testing/testing.go:1576
│ runtime.goexit
│ /usr/local/go/src/runtime/asm_amd64.s:1598
Next: (7) err2
Error types: (1) *errors.withStack (2) *errors.withNewMessage (3) *errors.joinError (4) *errors.withStack (5) *errors.errorString (6) *errors.withStack (7) *errors.errorString
*/
|
Why 为什么要新造轮子
pkg/errors
Go 语言内置的错误都不带堆栈,所以社区流行的错误库一般是 pkg/errors.
这个库提供了 New, Wrap, Wrapf 等许多方法,能够创建带堆栈的错误实例。然而它也有一些缺陷:
- 仅支持
%v, %+v, %s, %q 这些格式化动词,不支持 %x, %X, 也不支持 %#v, 带宽度 %10s 这种
- 在
%+v 详细模式打印时,包装错误和被包装错误如果堆栈重复,会重复打印
- 这个库现在已经停止更新:This repository has been archived by the owner on Dec 1, 2021. It is now read-only.
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
|
package main_test
import (
"testing"
"github.com/pkg/errors"
)
func TestErrors(t *testing.T) {
err := errors.New("pkgErr")
t.Logf("[%10s]", err.Error()) // [ pkgErr]
t.Logf("[%20s]", err) // [pkgErr]
t.Logf("[% X]", err) // []
t.Logf("[%#v]", err) // [pkgErr]
t.Logf("%+v", errors.Wrap(err, "prefix"))
/*
/path/to/errors_test.go:13: pkgErr
example_test.TestErrors
/path/to/errors_test.go:10
testing.tRunner
/usr/local/go/src/testing/testing.go:1576
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1598
prefix
example_test.TestErrors
/path/to/errors_test.go:15
testing.tRunner
/usr/local/go/src/testing/testing.go:1576
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1598
*/
}
|
cockroachdb/errors
This library aims to be used as a drop-in replacement to github.com/pkg/errors and Go's standard errors package. It also provides network portability of error objects, in ways suitable for distributed systems with mixed-version software compatibility.
cockroachdb/errors 这个库旨在替换 pkg/errors 和 Go 内置错误。它解决了 pkg/errors 上述的几个缺陷:
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
|
package main_test
import (
"testing"
"github.com/cockroachdb/errors"
)
func TestErrors2(t *testing.T) {
err := errors.New("cockroachdbErr")
t.Logf("[%20s]", err.Error()) // [ cockroachdbErr]
t.Logf("[%20s]", err) // [ cockroachdbErr]
t.Logf("[% X]", err) // [63 6F 63 6B 72 6F 61 63 68 64 62 45 72 72]
t.Logf("[%#v]", err)
/*
[&withstack.withStack{
cause: &errutil.leafError{msg:"cockroachdbErr"},
stack: &withstack.stack{0x1579dc8, 0x122db17, 0x10c11c1},
}]
*/
t.Logf("%+v", errors.Wrap(err, "prefix"))
/*
/path/to/cockroach_test.go:21: prefix: cockroachdbErr
(1) attached stack trace
-- stack trace:
| example_test.TestErrors2
| /path/to/cockroach_test.go:21
| [...repeated from below...]
Wraps: (2) prefix
Wraps: (3) attached stack trace
-- stack trace:
| example_test.TestErrors2
| /path/to/cockroach_test.go:10
| testing.tRunner
| /usr/local/go/src/testing/testing.go:1576
| runtime.goexit
| /usr/local/go/src/runtime/asm_amd64.s:1598
Wraps: (4) cockroachdbErr
Error types: (1) *withstack.withStack (2) *errutil.withPrefix (3) *withstack.withStack (4) *errutil.leafError
*/
}
|
这个库还提供了一些高级功能(我还没用过):支持跨网络传输错误、支持敏感数据打码等。
但它也有一些些小的缺陷:
- 依赖比较重,直接依赖 13 项,go.sum 文件达 103 行
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
|
module github.com/cockroachdb/errors
go 1.19
require (
github.com/cockroachdb/datadriven v1.0.2
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b
github.com/cockroachdb/redact v1.1.5
github.com/getsentry/sentry-go v0.18.0
github.com/gogo/googleapis v1.4.1 // gogoproto 1.2-compatible, for CRDB
github.com/gogo/protobuf v1.3.2
github.com/gogo/status v1.1.0
github.com/hydrogen18/memlistener v1.0.0
github.com/kr/pretty v0.3.1
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.1
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20230227214838-9b19f0bdc514 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
|
- multi-cause 错误格式化效果不好、错误信息含换行符时会丢失内容
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
|
package main_test
import (
"testing"
"github.com/cockroachdb/errors"
)
func TestMultiCause(t *testing.T) {
err1 := errors.New("err1\nnew\nline")
err2 := errors.New("err2\nnew\nline")
err := errors.Join(err1, err2)
t.Logf("%+v", errors.Wrap(err, "prefix"))
/*
/path/to/cockroach_test.go:47: prefix: err1
(1) attached stack trace
-- stack trace:
| example_test.TestMultiCause
| /path/to/cockroach_test.go:47
| testing.tRunner
| /usr/local/go/src/testing/testing.go:1576
Wraps: (2) prefix
Wraps: (3) attached stack trace
-- stack trace:
| example_test.TestMultiCause
| /path/to/cockroach_test.go:46
| testing.tRunner
| /usr/local/go/src/testing/testing.go:1576
Wraps: (4) err1
| new
| line
| err2
| new
| line
└─ Wraps: (5) attached stack trace
-- stack trace:
| example_test.TestMultiCause
| /path/to/cockroach_test.go:45
| [...repeated from below...]
└─ Wraps: (6) err2
| new
| line
└─ Wraps: (7) attached stack trace
-- stack trace:
| example_test.TestMultiCause
| /path/to/cockroach_test.go:44
| testing.tRunner
| /usr/local/go/src/testing/testing.go:1576
| runtime.goexit
| /usr/local/go/src/runtime/asm_amd64.s:1598
└─ Wraps: (8) err1
| new
| line
Error types: (1) *withstack.withStack (2) *errutil.withPrefix (3) *withstack.withStack (4) *join.joinError (5) *withstack.withStack (6) *errutil.leafError (7) *withstack.withStack (8) *errutil.leafError
*/
}
|
What 新造的轮子是什么样
不依赖其他库
不过,内部使用了 github.com/knz/go-fmtfwd 和 github.com/kr/pretty 的代码。
go.mod
1
2
3
|
module code.gopub.tech/errors
go 1.18
|
格式化为树形,而不是链表
支持多行错误、multi-cause 错误
以上述 cockroachdb/errors 例子重新用 code.gopub.tech/errors 执行,如下:
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
|
package main_test
import (
"testing"
"code.gopub.tech/errors"
)
func TestNewErrors(t *testing.T) {
err1 := errors.New("err1\nnew\nline")
err2 := errors.New("err2\nnew\nline")
err := errors.Join(err1, err2)
t.Logf("%+v", errors.Wrap(err, "prefix"))
/*
/path/to/new_errors_test.go:13: prefix: err1
new
line
err2
new
line
(1) attached stack trace
│ -- stack trace:
│ example_test.TestNewErrors
│ /path/to/new_errors_test.go:13
│ [...repeated from below...]
Next: (2) prefix
Next: (3) attached stack trace
│ -- stack trace:
│ example_test.TestNewErrors
│ /path/to/new_errors_test.go:12
│ [...repeated from below...]
Next: (4) err1
│ new
│ line
│ err2
│ new
│ line
├─ Wraps: (5) attached stack trace
│ │ -- stack trace:
│ │ example_test.TestNewErrors
│ │ /path/to/new_errors_test.go:10
│ │ [...repeated from below...]
│ Next: (6) err1
│ │ new
│ └─ line
└─ Wraps: (7) attached stack trace
│ -- stack trace:
│ example_test.TestNewErrors
│ /path/to/new_errors_test.go:11
│ testing.tRunner
│ /usr/local/go/src/testing/testing.go:1576
│ runtime.goexit
│ /usr/local/go/src/runtime/asm_amd64.s:1598
Next: (8) err2
│ new
└─ line
Error types: (1) *errors.withStack (2) *errors.withPrefix (3) *errors.withStack (4) *errors.joinError (5) *errors.withStack (6) *errors.errorString (7) *errors.withStack (8) *errors.errorString
*/
}
|
How 轮子是如何造的
兼容 pkg/errors 的堆栈
因为这个库使用范围太广了,如果对 pkg/errors 类型的错误进行包装后,无法打印其堆栈那将是不可接受的(点名批评内置的 fmt.Errorf("prefix: %w", pkgErr))。
查看(并 抄袭 借鉴)其源码,堆栈信息是通过 StackTrace() 方法打印的。
cockroachdb/errors 是直接引用了 pkg/errors 的 StackTrace 、Frame 等类型。
不过好在 StackTrace 实际就是 []uintptr,所以为了达成尽量不引入依赖的目的,直接使用了反射执行错误实例上 StackTrace() 方法以获取堆栈信息。
格式化为树形
查看 cockroachdb/errors 的源码,发现了它打印错误的套路,就是将各个错误类型的打印方法(Format(s fmt.State, verb rune)) 都代理给 FormatError() 去实现:
- 首先通过
Unwrap 递归地获取内层错误信息
- 然后构造整个错误链
- 然后输出错误链到一个缓冲区
- 最后使用外部指定的格式化动词输出缓冲区内容(这样就支持了
%10s, % X 等)
然而,这个库实现挺复杂的(里面也借鉴了x/xerrors ),可能是有历史包袱,go1.20 的 fmt.Errorf 新支持了使用多个 %w 包装多个错误,在这个库中打印的效果并不好(上面的示例可以看出)。
本来打算在它的基础上,支持多错误打印,但是写着写着发现它的逻辑太复杂了,干脆推倒重来:
- 首先通过
Unwrap()error 和 Unwrap()[]error 递归地获取最内层错误信息
- 然后构造出错误树!(单一 cause 时,就是只有一个子树)
- 先根顺序遍历错误树输出到缓冲区(这里调整树的格式费了好久)
- 输出缓冲区内容
其他一些改动,感兴趣的话可以参考源码。包括:
- 简化内部
state 的逻辑,去掉依据换行符检测是简单消息还是详细信息(就是这个逻辑,导致 cockroachdb/errors 对多行错误不友好)
- 将它从
x/errors继承来的 Formatter 改为 ErrorPrinter,并同步修改 Printer 接口,去除其 Detail 方法,转而新增 PrintDetail, PrintDetailf 方法(原先的 Detail() 实现也很费劲,调用到这里时需要使用 swtichOver 函数将 buf 缓冲区转换为 headBuf,一点都不直观。新的实现直接在一开始就区分出 simple 缓冲区和 detai 缓冲区)
Links
Reader Echoes
No comments yet