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

huangapple go评论90阅读模式
英文:

How to use pkg/errors to annotate error & pretty print stack trace in golang?

问题

考虑以下代码(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包生成错误,并正确使用fmt%+v%s来打印出堆栈跟踪。

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

英文:

Consider the following code (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)
	}
}

I originate the error with errors.New and annotate it with additional info using errors.Errorf.

It does what I want--record and print the stack trace & line number. However, the problem is that the output of log.Printf("%+v", err) is verbose and repetitive:

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

iiuc, errors package appends an additional copy of stack trace to the error every time of annotating the error, as can be seen in the below snippet

// 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

My desired output is

// 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

One way to achieve that is to only use the errors package to originate the error and then use fmt.Errorf with %+v to add additional info in the call stack (like this https://go.dev/play/p/OrWe6KUIL_m). However, it's error-prone and hard to enforce every developer to use this pattern in a large code base. Developers have to remember to use the errors package to originate the error and use fmt properly with %+v %s to print out the stack trace.

I'm wondering if this is the desired behavior (verbose and repetitive). And is it possible to consistently use the errors package to annotate errors along the call stack without worrying about appending repetitive stack trace copies (e.g., the errors magically knows the error already has a stack trace)?

答案1

得分: 2

打印错误时有 v 种格式说明符。

%s - 打印错误。如果错误有原因(Cause),则会递归打印。

%v - 只打印值,不打印字段名。在使用 Println 打印结构体时,默认使用此方式。

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

在你的例子中:

1️⃣ 使用堆栈文件创建新的错误(errors.New)。

2️⃣ 使用格式化的消息和该错误的堆栈创建新的错误(errors.Errorf)。

3️⃣ 使用该错误的堆栈创建新的错误(errors.WithStack)。

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

或者可以使用 fmt 和 github.com/pkg/errors 结合创建带有堆栈的错误。

以上是要翻译的内容。

英文:

there are v format specifiers for printing an error.

%s - print the error. If the error has a Cause it will be
printed recursively.

%v – It will print only values. The field name will not be printed. This is the default way of printing a struct when using Println
(print the error 👉🏻 If the error has a Cause it will be printed recursively.)

%+v – It will print both field and value.
(extended format. Each Frame of the error's StackTrace will be printed in detail.)

In your case:

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️⃣ create new error with stack file (errors.New)

2️⃣ create new error with "formatted" message and this error stack (errors.Errorf)

3️⃣ create new error with this error stack (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️⃣ start of the first error stack

2️⃣ start of the second error stack

3️⃣ start of the third error stack

You can simply create the "root" error with stack and then add message (wrap) "root" error

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)
    }
}

<kbd>PLAYGROUND</kbd>

or

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

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

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

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

<kbd>PLAYGROUND</kbd>

答案2

得分: 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, "..."),以及
  • 导入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)
    }
}
英文:

I don't particularly like github.com/pkg/errors, so I wrote my own package with similar behavior where I can control the format of my stack.

You can find my package in github.com/go-msvc/errors and copy or change it as you like.

It does not have a errors.New(), only errors.Error() which does the same. You can change that in your copy to be consistent with the standard one (I will too).

I also wrap errors with errors.Wrapf(err, "...xxx failed..."). If you just append inside a string like you did, the formatter cannot traverse the layers.

So, when I take your main.go and change:

  • New() to Error(),
  • Errorf("....: %+v", err) to Wrapf(err, "..."), and
  • import github.com/go-msvc/errors,
    Then it outputs this:

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

That works for me and avoids lots of clutter in the stack.

If you want newlines, you can add that too, but I prefer my error messages in my log files not to have newlines.

Code:

package main

import (
    &quot;log&quot;

    &quot;github.com/go-msvc/errors&quot;
)

func myError() error {
    return errors.Error(&quot;failing unconditionally&quot;)
}

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

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

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

huangapple
  • 本文由 发表于 2022年7月13日 12:08:46
  • 转载请务必保留本文链接:https://go.coder-hub.com/72960838.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定