如何使用pkg/errors在Golang中注释错误并漂亮地打印堆栈跟踪?

4
考虑以下代码(https://go.dev/play/p/hDOyP3W_lqW
package main

import (
    "log"

    "github.com/pkg/errors"
)

func myError() error {
    return errors.New("failing unconditionally")
}

func myError1() error {
    return errors.Errorf("annotate with additional debug info: %+v", myError())
}

func myError2() error {
    return errors.Errorf("extra debug info: %+v", myError1())
}

func main() {
    if err := myError2(); err != nil {
        log.Printf("%+v", err)
    }
}

我用errors.New创建错误并使用errors.Errorf添加额外信息。
它能够记录和打印堆栈跟踪和行号,符合我的需求。但是,问题在于log.Printf("%+v", err)的输出冗长而重复。
2009/11/10 23:00:00 extra debug info: annotate with additional debug info: failing unconditionally
main.myError
    /tmp/sandbox3329712514/prog.go:10
main.myError1
    /tmp/sandbox3329712514/prog.go:14
main.myError2
    /tmp/sandbox3329712514/prog.go:18
main.main
    /tmp/sandbox3329712514/prog.go:22
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1571
main.myError1
    /tmp/sandbox3329712514/prog.go:14
main.myError2
    /tmp/sandbox3329712514/prog.go:18
main.main
    /tmp/sandbox3329712514/prog.go:22
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1571
main.myError2
    /tmp/sandbox3329712514/prog.go:18
main.main
    /tmp/sandbox3329712514/prog.go:22
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1571

根据我的理解,errors包在每次注释错误时都会将额外的堆栈跟踪复制附加到错误中,如下面的代码片段所示。
// repetitive (thrice) error stack
main.myError
    /tmp/sandbox3329712514/prog.go:10
main.myError1
    /tmp/sandbox3329712514/prog.go:14
main.myError2
    /tmp/sandbox3329712514/prog.go:18
main.main
    /tmp/sandbox3329712514/prog.go:22
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1571
main.myError1
    /tmp/sandbox3329712514/prog.go:14
main.myError2
    /tmp/sandbox3329712514/prog.go:18
main.main
    /tmp/sandbox3329712514/prog.go:22
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1571
main.myError2
    /tmp/sandbox3329712514/prog.go:18
main.main
    /tmp/sandbox3329712514/prog.go:22
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1571


我的期望输出是:
// Desired output
2009/11/10 23:00:00 extra debug info: annotate with additional debug info: failing unconditionally
main.myError
    /tmp/sandbox3329712514/prog.go:10
main.myError1
    /tmp/sandbox3329712514/prog.go:14
main.myError2
    /tmp/sandbox3329712514/prog.go:18
main.main
    /tmp/sandbox3329712514/prog.go:22
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:250
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1571

实现这一目标的方法之一是只使用errors包来发出错误,然后在调用堆栈中使用fmt.Errorf%+v添加附加信息(例如https://go.dev/play/p/OrWe6KUIL_m)。但是,这种方法容易出错,在大型代码库中强制每个开发人员都使用这种模式很困难。开发人员必须记住使用errors包来发出错误,并使用%+v %s正确打印出堆栈跟踪。

我想知道这是否是期望的行为(冗长重复)。是否有可能始终使用errors包来注释沿着调用堆栈的错误,而不必担心附加重复的堆栈跟踪副本(例如,errors自动知道错误已经有了堆栈跟踪)?


1
“New”、“Errorf”和“WithStack”函数实际上都记录了堆栈跟踪,这就是为什么最终会得到3个堆栈跟踪的原因。我不太确定您需要什么样的输出,但您应该能够使用“Wrap”函数而不是现在使用的所有3个函数。 - Gari Singh
1
谢谢@GariSingh,我已更新我的问题并添加了我想要的输出。我还添加了一个链接到一个做我想要的事情的更新版本代码。然而,那个代码容易出错,我必须在“错误”和“fmt”之间切换,并正确使用“%+v”。所以我想知道是否可以一致地使用错误包来注释沿调用堆栈的错误,而不必担心附加重复副本的堆栈跟踪。 - Zizheng Wu
3个回答

2

v格式说明符可用于打印错误信息。

%s - 打印错误信息。如果错误有原因,则会递归打印。

%v - 只打印值,不打印字段名。这是使用Println打印结构体的默认方式。(打印错误信息。如果错误有原因,则会递归打印。)

%+v - 打印字段和值。(扩展格式。将打印错误的每个StackTrace帧的详细信息。)

在您的情况下:

func myerror() error {
    return errors.New("failing unconditionally") // 1️⃣
}

func myerror1() error {
    return errors.Errorf("annotate with additional debug info: %+v", myerror()) // 2️⃣
}

func myerror2() error {
    return errors.WithStack(myerror1()) // 3️⃣
}

1️⃣ 使用文件堆栈信息创建新的error (errors.New)

2️⃣ 使用“格式化”的消息和此error的堆栈信息创建新的error (errors.Errorf)

3️⃣ 使用此error的堆栈信息创建新的error (errors.WithStack)

2022/07/13 11:42:03 annotate with additional debug info: failing unconditionally
github.com/kozmod/idea-tests/core/errors.myerror
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:10 // 1️⃣
github.com/kozmod/idea-tests/core/errors.myerror1
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:14
github.com/kozmod/idea-tests/core/errors.myerror2
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:18
github.com/kozmod/idea-tests/core/errors.TestStack.func1
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:35
testing.tRunner
    /Library/GoLang/go1.18.2.darwin-amd64/src/testing/testing.go:1439
runtime.goexit
    /Library/GoLang/go1.18.2.darwin-amd64/src/runtime/asm_amd64.s:1571
github.com/kozmod/idea-tests/core/errors.myerror1
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:14 // 2️⃣
github.com/kozmod/idea-tests/core/errors.myerror2
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:18
github.com/kozmod/idea-tests/core/errors.TestStack.func1
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:35
testing.tRunner
    /Library/GoLang/go1.18.2.darwin-amd64/src/testing/testing.go:1439
runtime.goexit
    /Library/GoLang/go1.18.2.darwin-amd64/src/runtime/asm_amd64.s:1571
github.com/kozmod/idea-tests/core/errors.myerror2
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:18 // 3️⃣
github.com/kozmod/idea-tests/core/errors.TestStack.func1
    /Users/19798572/GolandProjects/idea-tests/core/errors/stack_test.go:35
testing.tRunner
    /Library/GoLang/go1.18.2.darwin-amd64/src/testing/testing.go:1439
runtime.goexit
    /Library/GoLang/go1.18.2.darwin-amd64/src/runtime/asm_amd64.s:1571

1️⃣ 第一个错误堆栈的开始

2️⃣ 第二个错误堆栈的开始

3️⃣ 第三个错误堆栈的开始

您可以简单地创建带有堆栈的“根”错误,然后添加消息(包装)“根”错误。

func myerror3() error {
    return errors.New("failing unconditionally")
}

func myerror4() error {
    return errors.WithMessage(myerror3(), "annotate with additional debug info")
}

func myerror5() error {
    return errors.WithMessage(myerror4(), "myerror5")
}

func main() {
    if err := myerror5(); err != nil {
        log.Printf("%+v", err)
    }
}

PLAYGROUND

或者

func myError() error {
    // create error (github.com/pkg/errors + fmt) with stack (message)
    return fmt.Errorf("%+v", errors.New("failing unconditionally"))
}

func myError1() error {
    return fmt.Errorf("annotate with additional debug info: %v", myError())
}

func myError2() error {
    return fmt.Errorf("extra debug info: %v", myError1())
}

func main() {
    if err := myError2(); err != nil {
        log.Printf("%v", err)
    }
}

PLAYGROUND


2
谢谢您的回答!了解errors.WithMessage很好。不幸的是,输出结果并不理想。无条件失败 ...10行堆栈跟踪 注释附加调试信息 myerror5我更新了我的问题,并添加了一个链接到一个更新版本的代码,它可以实现我想要的功能。然而,该代码容易出错,我必须在错误和fmt之间切换,并正确使用%+v。因此,我想知道是否有可能始终使用errors包来注释沿着调用堆栈的错误,而不必担心追加重复的堆栈跟踪副本。 - Zizheng Wu
1
@Zizheng Wu - 由于 github.com/pkg/errors 的实现,这是不可能的。唯一的方法是使用组合 errors/fmt [PLAYGROUND](https://go.dev/play/p/dpIOmHIEKFa)(首先使用 errors/fmt 创建带有堆栈的第一个错误,然后使用 fmt 包装它)。 - kozmo
2
谢谢@kozmo!我发现golang.org/x/xerrorsgithub.com/pkg/errors更可靠,并且可以完全实现我想要的功能(始终使用%+v而不必担心多个堆栈跟踪副本):https://go.dev/play/p/cGeZCOuCJ4s - Zizheng Wu

0

我不是特别喜欢github.com/pkg/errors,所以我写了一个类似的包,可以控制我的堆栈格式。

你可以在github.com/go-msvc/errors中找到我的包,并按照自己的需求进行复制或更改。

它没有errors.New(),只有errors.Error(),功能相同。你可以在你的副本中将其更改为与标准库一致(我也会这样做)。

我还使用errors.Wrapf(err, "...xxx failed...")来包装错误。如果你只是像你之前那样在字符串中添加,格式化程序无法遍历层级。

因此,当我拿起你的main.go并更改:

  • New()为Error(),
  • Errorf("....: %+v", err)为Wrapf(err, "..."),以及
  • import github.com/go-msvc/errors,

然后输出如下:

2022/07/14 13:35:33 main.go(18):extra debug info because main.go(14):annotate with additional debug info because main.go(10):failing unconditionally

这对我很有效,避免了堆栈中的大量混乱信息。

如果你想要换行符,也可以添加,但我更喜欢我的错误消息在日志文件中没有换行符。

代码:

package main

import (
    "log"

    "github.com/go-msvc/errors"
)

func myError() error {
    return errors.Error("failing unconditionally")
}

func myError1() error {
    return errors.Wrapf(myError(), "annotate with additional debug info")
}

func myError2() error {
    return errors.Wrapf(myError1(),"extra debug info")
}

func main() {
    if err := myError2(); err != nil {
        log.Printf("%+v", err)
    }
}

0
我之前做了一个名为gitlab.com/tozd/go/errors github.com/pkg/errors的精神继承者,解决了一系列github.com/pkg/errors存在的问题。以下是其中两个相关问题:
  • 我的错误包只在您封装的错误不包含堆栈跟踪时记录堆栈跟踪(如果您想强制再次记录堆栈跟踪,可以使用errors.Wrap)。
  • 您可以在errors.Errorf中使用%w格式化动词,将原始错误与标准的Go封装进行包装。
您可以将其作为github.com/pkg/errors的几乎完全替代品使用。因此,您只需更改导入并使用%w,上面的示例就会正常工作
package main

import (
    "log"

    "gitlab.com/tozd/go/errors"
)

func myError() error {
    return errors.New("failing unconditionally")
}

func myError1() error {
    return errors.Errorf("annotate with additional debug info: %w", myError())
}

func myError2() error {
    return errors.Errorf("extra debug info: %w", myError1())
}

func main() {
    if err := myError2(); err != nil {
        log.Printf("%+v", err)
    }
}

输出:

2009/11/10 23:00:00 extra debug info: annotate with additional debug info: failing unconditionally
main.myError
    /tmp/sandbox3373322165/prog.go:10
main.myError1
    /tmp/sandbox3373322165/prog.go:14
main.myError2
    /tmp/sandbox3373322165/prog.go:18
main.main
    /tmp/sandbox3373322165/prog.go:22
runtime.main
    /usr/local/go-faketime/src/runtime/proc.go:267
runtime.goexit
    /usr/local/go-faketime/src/runtime/asm_amd64.s:1650

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接