go get code.gopub.tech/errors
- 源码:https://github.com/pub-go/errors
- 镜像:https://gitee.com/pub-go/errors
- 文档:https://pkg.go.dev/code.gopub.tech/errors
Show me the code:
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.
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 上述的几个缺陷:
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 行
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 错误格式化效果不好、错误信息含换行符时会丢失内容
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
module code.gopub.tech/errors
go 1.18
格式化为树形,而不是链表
支持多行错误、multi-cause 错误
以上述 cockroachdb/errors 例子重新用 code.gopub.tech/errors
执行,如下:
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
- code.gopub.tech/errors
- pkg/errors
- x/xerrors
- cockroachdb/errors
- Go 标准错误 API — CockroachDB errors 库(第1篇)
- Go 格式化 API — CockroachDB errors 库(第2篇)
- Go error 打印灾难 — CockroachDB errors 库(第3篇)
- 除了 fmt.Errorf() 之外—Go 中的日常错误对象:CockroachDB errors 库(第4篇)
声明
- 本作品采用署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。除非特别注明, 霖博客文章均为原创。
- 转载请保留本文(《造一个 Go 语言错误库的轮子》)链接地址: https://youthlin.com/?p=1868
- 订阅本站:https://youthlin.com/feed/