errx

package module
v1.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2023 License: MIT Imports: 6 Imported by: 3

README

errx - 更精准的定位和跟踪错误

GoDoc Go codecov License GoVersion Go Report Card

功能:

  • 封装引起错误的错误。
  • 为错误追加方法调用栈信息。
  • 业务预定义异常 BizError
  • 用于处理 recover() 结果的 PreserveRecover 方法。

安装:

go get -u github.com/cmstar/go-errx@latest

调用栈和错误链

Go 程序通常小而精,更多的用于中间件和系统编程,但有时仍会被用在上层的复杂业务里,和 Java 、 .net 同台,此时 Go 的错误处理模式就变得捉襟见肘起来。也许这就不是 Go 的设计意图,但真实的编码场景里,我们免不了碰到这样的情况。

Go 的 error 只是“不太特殊”的值而已(Errors are values),它太过于普通以至于不能像 Java/.net 的 Exception 一样携带足够多的信息。

在复杂的上层业务中,快速定位错误的位置显得极为重要,有时可以用运行性能换工作效率。


Wrap 方法

errx.Wrap 为错误添加更多的细节,返回一个 StackfulError 接口的实现,它包含方法:

  • Cause:记录引起错误的错误,类似 Java 的 Throwable.getCause() 或 .net 的 Exception.InnerException
  • Stack:记录创建错误(即调用 Wrap 方法)时的方法调用栈,类似 Java 的 Exception.printStackTrace() 或 .net 的 Exception.StackTrace
  • Error:即标准库的 error.Error() ,但它包含了 CauseStack 格式化后的信息。输出格式见下文《Describe 方法》。
  • ErrorWithoutStack:只包含错误的描述,不包含 Stack 的信息。

调用栈信息使用标准库的 runtime.CallersFrames 方法获取,有一定的性能开销。

BizError

在业务交互中,我们可能需要根据错误的类别进行不同的处理,原始的 error 等同于一个字符串,难以判断和分类。 errx 包定义了 BizError ,以便对错误进行分类。它是一个特殊的 error ,可通过 errx.NewBizError 方法创建。

BizError 提供:

  • 为每个错误添加一个整数型的错误码 Code ,以便更精准的对错误进行分类和定位,特别是在日志搜索时。通常0表示没有错误,其余值表示有错误,值由具体业务指定。
  • 包含调用栈信息 Stack
  • 包含 Cause ,即引起此错误的错误。

BizError.Error() 返回值格式为: (Code) Message ,不包含 CauseStack

BizError 的使用样例可参考 GoDoc 示例

go-webapi 框架使用 BizError 区分需要返回的业务错误和其他内部错误。

方法

Describe 方法

errx.Describeerrx.Wrap 添加的信息抽取出来,形成一段完整的错误描述,它包含各层级错误的信息及调用栈。

格式为:

最外层错误描述
--- 最外层错误的调用栈信息
=== 第1层内部错误的描述
--- 第1层内部错误的调用栈信息
=== 第2层内部错误的描述
--- 第2层内部错误的调用栈信息
...(逐层展示)
=== 最内层错误的描述
--- 最内层错误的描述的调用栈信息

实际示例可参考 GoDoc 示例


总体而言,就是让 Go 的 error 更像 Java/.net 的 Exception

当一个 errorWrap 之后返回给其调用者,调用者再次使用 Wrap 并返回给更上层的调用者, error 就形成了一个链条。

PreserveRecover 方法

我们可能需要利用应对 panic ,并将相关的错误信息保留下来,代码如下:

func do() (err error) {
    defer func() {
        r := recover()
        if r == nil {
            return
        }

        // 留存错误,丢失了调用栈信息。
        err = fmt.Errorf("do: %v", r)
    }()

    somethingThatMayPanic()
    return nil
}

利用 PreserveRecover 方法,这段代码可以简化成这样,当 recover() 的结果不是 nil 时,它会被 Wrap() 在一个错误里:

func do() (err error) {
    defer func() {
        // 留存错误,并保留调用栈信息。若 recover() 结果为 nil ,则 err 也是 nil 。
        err = errx.PreserveRecover("do", recover())
    }()

    somethingThatMayPanic()
    return nil
}
Run/RunE 方法

用于执行一个可能会 panic 的方法,自动添加 defer 过程,并通过 PreserveRecover 方法捕获错误。

func do() {
    err := errx.Run(func() {
        panic("oops!")
    })
    fmt.Println(err)
}

Documentation

Overview

errx 包提供一组类型与方法,用于将错误信息封装起来形成错误链,以便在程序中更精准的定位和跟踪错误。

Example (ErrorChain)
package main

import (
	"bufio"
	"errors"
	"fmt"
	"strings"

	"github.com/cmstar/go-errx"
)

func main() {
	// 当前示例演示如何利用 Wrap 方法封装错误,并利用 Describe 方法获取完整的错误描述。

	// 调用顺序 A() -> B() -> Source() 。
	err := A()

	// 获取带有调用栈的错误描述。
	// 也可以通过 fmt.Sprintf("%+v", err) 得到一样的结果。
	msg := errx.Describe(err)

	// 简化一下输出。
	msg = simplify(msg)
	fmt.Println(msg)

}

//go:noinline
func A() error {
	e := B(0)
	return errx.Wrap("from A", e)
}

//go:noinline
func B(int) error {
	e := Source()
	return errx.Wrap("from B", e)
}

//go:noinline
func Source() error {
	return errors.New("the original error")
}

func simplify(stack string) string {
	/*
		完整的信息类似:
		  from A: from B: the original error
		  --- [/home/my/go-errx/example_test.go:42] go-errx_test.A
		  [/home/my/go-errx/example_test.go:16] go-errx_test.Example_errorChain
		  [/path/testing/run_example.go:64] testing.runExample
		  [/path/testing/example.go:44] testing.runExamples
		  [/path/testing/testing.go:1505] testing.(*M).Run
		  [_testmain.go:57] main.main

		过于冗长,耦合物理路径难以断言输出,将其简化,仅留下方法名,并去掉非本地代码的部分。
	*/
	s := bufio.NewScanner(strings.NewReader(stack))
	b := new(strings.Builder)
	for s.Scan() {
		line := s.Text()
		if strings.Contains(line, "testing.") || strings.Contains(line, "main") {
			continue
		}

		idx := strings.Index(line, "[")
		if idx >= 0 {
			idxSlash := strings.LastIndex(line, " ")
			if idxSlash >= 0 {
				line = line[:idx] + line[idxSlash+1:]
			}
		}

		b.WriteString(line)
		b.WriteRune('\n')
	}
	return b.String()
}
Output:

from A: from B: the original error
--- go-errx_test.A
go-errx_test.Example_errorChain
=== from B: the original error
--- go-errx_test.B
go-errx_test.A
go-errx_test.Example_errorChain
=== the original error

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Describe

func Describe(err error) string

Describe 返回一个字符串描述给定的错误。如果给定 nil ,返回空字符串。

递归使用 errors.Unwrap() 获取内部错误,并追加在描述信息上。如果错误是 StackfulError ,则描述携带调用栈信息。 若不能获取到对应的信息,则该部分省略。

可通过此方法获取完整的错误链信息。

输出格式为:

最外层错误描述
--- 最外层错误的调用栈信息
=== 第1层内部错误的描述
--- 第1层内部错误的调用栈信息
=== 第2层内部错误的描述
--- 第2层内部错误的调用栈信息
...(逐层展示)
=== 最内层错误的描述
--- 最内层错误的描述的调用栈信息

末尾总是一个空行。

func Run added in v1.5.0

func Run(f func()) (err error)

执行给定的函数。 若函数成功执行,返回 nil ;若函数 panic ,则通过 PreserveRecover 捕获并返回对应的错误。

func RunE added in v1.5.0

func RunE(f func() error) (err error)

执行带有一个 error 返回值的的函数。 若函数成功执行,返回函数的返回值;若函数 panic ,则通过 PreserveRecover 捕获并返回对应的错误。

Types

type BizError

type BizError interface {
	StackfulError

	// Code 表示错误码。通常 0 表示没有错误。
	Code() int

	// Message 返回错误的描述信息。
	Message() string

	// Cause 记录引起此错误的内部错误。
	// 当一个 error 可以被作为 BizError 对待时,可创建 BizError 并设置此字段。
	// 若没有内部错误,为 nil 。
	Cause() error
}

BizError 是一个 error 。增加了错误码等属性,可更精确的表示一个预定义的错误。 BizError 实现 StackfulError ,可以携带调用栈信息。 其 Error() 方法不包含调用栈信息和内部错误的信息,目的是隐藏内部细节,仅用于输出业务信息。

Example
package main

import (
	"errors"
	"fmt"

	"github.com/cmstar/go-errx"
)

func main() {
	// 当前示例演示如何通过 BizError 对错误进行分类。

	printErr := func(e error) {
		biz, ok := e.(errx.BizError)
		if ok {
			switch biz.Code() {
			case 1:
				fmt.Println("BizError1: " + biz.Error())
			case 2:
				fmt.Println("BizError2: " + biz.Error())
			}
		} else {
			fmt.Println("non-BizError: " + e.Error())
		}
	}
	printErr(GetError(true, 1))
	printErr(GetError(true, 2))
	printErr(GetError(false, 0))

}

func GetError(biz bool, code int) error {
	if biz {
		return errx.NewBizError(code, "hello", nil)
	}
	return errors.New("other error")
}
Output:

BizError1: (1) hello
BizError2: (2) hello
non-BizError: other error

func NewBizError

func NewBizError(code int, message string, cause error) BizError

NewBizError 创建一个 BizError ,给定错误码、错误信息和引起此错误的错误。 cause 指定引发此错误的错误,可以为 nil 。 此方法创建的 BizError 会包含方法调用栈信息。

func NewBizErrorWithoutStack

func NewBizErrorWithoutStack(code int, message string, cause error) BizError

NewBizErrorWithoutStack 创建一个 BizError ,给定错误码、错误信息和引起此错误的错误。 cause 指定引发此错误的错误,可以为 nil 。 和 NewBizError() 类似,但不带调用栈信息, BizError.Stack() 返回空字符串。

type ErrorCause

type ErrorCause struct {
	Err error
}

ErrorCause 用于封装一个 error ,表示引起另一个错误的错误,它支持 errors.Unwrap() 。

func (ErrorCause) Cause

func (e ErrorCause) Cause() error

Cause 同 Unwrap()。返回引起当前错误的错误。

func (ErrorCause) Unwrap

func (e ErrorCause) Unwrap() error

Unwrap 返回引起当前错误的错误。

type ErrorStack

type ErrorStack struct {
	// contains filtered or unexported fields
}

ErrorStack 用于存放调用栈信息,以便实现 StackfulError 。 输出调用栈格式为(末尾有一个空行):

[file0:line] func0
[file1:line] func1
[file2:line] func2

func GetErrorStack

func GetErrorStack(skip int) ErrorStack

GetErrorStack 创建一个带有调用栈信息的 ErrorStack 。 调用栈信息使用 runtime.CallersFrames() 获取,skip 参数传递给 runtime.Callers() 。 要跳过当前函数,至少为 2 :分别跳过 runtime.Callers() 和当前函数。

func (ErrorStack) Stack

func (e ErrorStack) Stack() string

Stack 实现 StackfulError.Stack() 。

type ErrorWrapper

type ErrorWrapper struct {
	ErrorCause
	ErrorStack
	// contains filtered or unexported fields
}

ErrorWrapper 是一个 StackfulError ,封装另一个 error ,其表示引起当前错误的原因。

func (*ErrorWrapper) Error

func (w *ErrorWrapper) Error() string

Error 返回以 Describe() 的格式输出错误信息。

func (*ErrorWrapper) ErrorWithoutStack added in v1.4.0

func (w *ErrorWrapper) ErrorWithoutStack() string

Message 返回错误的描述信息,但不包含 Stack 。

返回格式如下,当前实例的 message 为空时,前置的“message:”部分被省略。

  • 若 cause 为 nil,则仅返回当前实例的错误信息;
  • 若 cause 为 StackfulError, 则返回: message: cause.ErrorWithoutStack() ;
  • 若 cause 不是 StackfulError, 则返回: message: cause.Error() 。

func (*ErrorWrapper) Format added in v1.2.0

func (w *ErrorWrapper) Format(f fmt.State, verb rune)

Format 实现 fmt.Formatter.Formats() 。 支持:

%s      输出 ErrorWithoutStack()
%q      输出 strconv.Quote(ErrorWithoutStack())
%v/%+v  输出 Error()
other   输出 BADFORMAT: ErrorWithoutStack()

type StackfulError

type StackfulError interface {
	error

	// ErrorWithoutStack 返回错误的描述信息,但不包含 Stack 。
	ErrorWithoutStack() string

	// Cause 返回引起当前错误的错误。
	Cause() error

	// Stack 返回调用栈信息。若未记录调用栈,可返回空值。
	Stack() string
}

StackfulError 是一个包含调用栈信息的 error 。

通常, Error() 方法返回带有调用栈信息的错误描述。 可通过 ErrorWithoutStack() 获取没有调用栈的错误描述。

func PreserveRecover added in v1.3.0

func PreserveRecover(message string, recovered interface{}) StackfulError

PreserveRecover 用于封装从 panic 中 recover 的数据,返回 StackfulError 。 此方法的调用应放在 defer 过程里。

Example
demo := func() (e error) {
	defer func() {
		e = errx.PreserveRecover("oops, it panics", recover())
	}()

	panic(123)
}

err := demo()
fmt.Println(fmt.Sprintf("%+v", err)[:25] + "the callstack ...")
Output:

oops, it panics: 123
--- the callstack ...

func Wrap

func Wrap(message string, cause error) StackfulError

Wrap 封装给定的 error ,返回 StackfulError 。 错误信息的格式为: message: cause.Error() 。若 cause 为 nil,则仅返回 message 。

得到的 StackfulError.Stack() 有一个固定的开头“--- ”,末尾会有一个空行。格式为:

--- stack text
Example
origin := errors.New("the cause")
w := errx.Wrap("something wrong", origin)

fmt.Println("Cause():")
fmt.Println(w.Cause())

fmt.Println("\nErrorWithoutStack() has no stack:")
fmt.Println(w.ErrorWithoutStack())

fmt.Println("\nError() has stack:")
e := w.Error()
index := strings.Index(e, "--- ")
fmt.Println(e[:index] + "--- the callstack ...")
Output:

Cause():
the cause

ErrorWithoutStack() has no stack:
something wrong: the cause

Error() has stack:
something wrong: the cause
--- the callstack ...

func WrapWithoutStack

func WrapWithoutStack(message string, cause error) StackfulError

WrapWithoutStack 封装给定的 error 。和 Wrap() 类似,但不带有调用栈信息。 错误信息的格式为: message: cause.Error() 。若 cause 为 nil,则仅返回 message 。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL