分类
Go

造一个 Go 语言错误库的轮子

go get 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.
这个库提供了 NewWrapWrapf 等许多方法,能够创建带堆栈的错误实例。然而它也有一些缺陷:

  • 仅支持 %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() 去实现:

  1. 首先通过 Unwrap 递归地获取内层错误信息
  2. 然后构造整个错误链
  3. 然后输出错误链到一个缓冲区
  4. 最后使用外部指定的格式化动词输出缓冲区内容(这样就支持了 %10s% X 等)

然而,这个库实现挺复杂的(里面也借鉴了x/xerrors ),可能是有历史包袱,go1.20 的 fmt.Errorf 新支持了使用多个 %w 包装多个错误,在这个库中打印的效果并不好(上面的示例可以看出)。

本来打算在它的基础上,支持多错误打印,但是写着写着发现它的逻辑太复杂了,干脆推倒重来:

  1. 首先通过 Unwrap()error 和 Unwrap()[]error 递归地获取最内层错误信息
  2. 然后构造出错误树!(单一 cause 时,就是只有一个子树)
  3. 先根顺序遍历错误树输出到缓冲区(这里调整树的格式费了好久)
  4. 输出缓冲区内容

其他一些改动,感兴趣的话可以参考源码。包括:

  • 简化内部 state 的逻辑,去掉依据换行符检测是简单消息还是详细信息(就是这个逻辑,导致 cockroachdb/errors 对多行错误不友好)
  • 将它从 x/errors继承来的 Formatter 改为 ErrorPrinter,并同步修改 Printer 接口,去除其 Detail 方法,转而新增 PrintDetailPrintDetailf 方法(原先的 Detail() 实现也很费劲,调用到这里时需要使用 swtichOver 函数将 buf 缓冲区转换为 headBuf,一点都不直观。新的实现直接在一开始就区分出 simple 缓冲区和 detai 缓冲区)

Links


发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

[/鼓掌] [/难过] [/调皮] [/白眼] [/疑问] [/流泪] [/流汗] [/撇嘴] [/抠鼻] [/惊讶] [/微笑] [/得意] [/大兵] [/坏笑] [/呲牙] [/吓到] [/可爱] [/发怒] [/发呆] [/偷笑] [/亲亲]