错误组件

gerrors 组件提供了强大、丰富、统一的错误处理能力。该组件也是统一的错误处理组件,如果存在错误返回时,均带有堆栈信息,方便开发者快速定位问题。

常用方法

错误创建

New/Newf

New(text string) error
Newf(format string, args ...any) error

Wrap/Wrapf

func Wrap(err error, text string) error
func Wrapf(err error, format string, args ...any) error

NewSkip/NewSkipf

func NewSkip(skip int, text string) error
func NewSkipf(skip int, format string, args ...any) error

错误码相关方法

func NewCode(code int, text string) error
func NewCodef(code int, format string, args ...any) error
func NewCodeSkip(code, skip int, text string) error
func NewCodeSkipf(code, skip int, format string, args ...any) error
func WrapCode(code int, err error, text string) error
func WrapCodef(code int, err error, format string, args ...any) error

堆栈特性

错误堆栈

标准库的 error 错误实现比较简单,无法进行堆栈追溯,对于产生错误时的上层调用者来讲不是很友好,无法获得错误的调用链详细信息。gerror 支持错误堆栈记录,通过 New/NewfWrap/Wrapf 等方法均会自动记录当前错误产生时的堆栈信息。

示例:

package main

import (
    "fmt"

    "github.com/camry/g/gerrors/gerror"
)

func OpenFile() error {
    return gerror.New("permission denied")
}

func OpenConfig() error {
    return gerror.Wrap(OpenFile(), "configuration file opening failed")
}

func ReadConfig() error {
    return gerror.Wrap(OpenConfig(), "reading configuration failed")
}

func main() {
    fmt.Printf("%+v", ReadConfig())
}

// Output:
// reading configuration failed: configuration file opening failed: permission denied
// 1. reading configuration failed
// 2. configuration file opening failed
// 3. permission denied
//    1).  main.OpenFile
//         E:/project/Weition/go-example/main.go:10
//    2).  main.OpenConfig
//         E:/project/Weition/go-example/main.go:14
//    3).  main.ReadConfig
//         E:/project/Weition/go-example/main.go:18
//    4).  main.main
//         E:/project/Weition/go-example/main.go:22

可以看到,调用端可以通过Wrap方法将底层的错误信息进行层级叠加,并且包含完整的错误堆栈信息。

HasStack 判断错误是否带堆栈

HasStack(err error) bool
func ExampleHasStack() {
	err1 := errors.New("sql error")
	err2 := gerror.New("write error")
	fmt.Println(gerror.HasStack(err1))
	fmt.Println(gerror.HasStack(err2))

	// Output:
	// false
	// true
}

Stack 获取堆栈信息

Stack(err error) string
func ExampleStack() {
    var err error
    err = errors.New("sql error")
    err = gerror.Wrap(err, "adding failed")
    err = gerror.Wrap(err, "api calling failed")
    fmt.Println(gerror.Stack(err))

    // Output:
    // 1. api calling failed
    //    1).  main.ExampleStack
    //         E:/project/Weition/go-example/main.go:14
    // 2. adding failed
    //    1).  main.ExampleStack
    //         E:/project/Weition/go-example/main.go:13
    //    2).  main.main
    //         E:/project/Weition/go-example/main.go:28
    // 3. sql error
}

Current 获取当前 error

Current(err error) error
func ExampleCurrent() {
  	var err error
	err = errors.New("sql error")
	err = gerror.Wrap(err, "adding failed")
	err = gerror.Wrap(err, "api calling failed")
	fmt.Println(err)
	fmt.Println(gerror.Current(err))

	// Output:
	// api calling failed: adding failed: sql error
	// api calling failed
}

Next/Unwrap 获取下一层 error

Next(err error) error
func ExampleNext() {
	var err error
	err = errors.New("sql error")
	err = gerror.Wrap(err, "adding failed")
	err = gerror.Wrap(err, "api calling failed")

	fmt.Println(err)

	err = gerror.Next(err)
	fmt.Println(err)

	err = gerror.Next(err)
	fmt.Println(err)

	// Output:
	// api calling failed: adding failed: sql error
	// adding failed: sql error
	// sql error
}
func IsGrpcErrorNotFound(err error) bool {
	if err != nil {
		for e := err; e != nil; e = gerror.Unwrap(e) {
			if s, ok := status.FromError(e); ok && s != nil && s.Code() == codes.NotFound {
				return true
			}
		}
	}
	return false
}

Cause 获取根错误 error

Cause(err error) error
package main

import (
	"fmt"
	"github.com/camry/g/gerrors/gerror"
)

func OpenFile() error {
	return gerror.New("permission denied")
}

func OpenConfig() error {
	return gerror.Wrap(OpenFile(), "configuration file opening failed")
}

func ReadConfig() error {
	return gerror.Wrap(OpenConfig(), "reading configuration failed")
}

func main() {
	fmt.Println(gerror.Cause(ReadConfig()))
}

// Output:
// permission denied

错误比较

Equal 比较方法

错误对象支持比较,主要通过以下方法:

func Equal(err, target error) bool

接口定义

如果自定义的错误数据结构需要支持比较,需要自定义的错误结构实现以下接口:

Equal(target error) bool

使用示例

func ExampleEqual() {
	err1 := errors.New("permission denied")
	err2 := gerror.New("permission denied")
	err3 := gerror.NewCode(gcode.CodeNotAuthorized, "permission denied")
	fmt.Println(gerror.Equal(err1, err2))
	fmt.Println(gerror.Equal(err2, err3))

	// Output:
	// true
	// false
}

Is 包含判断

错误对象支持包含判断,主要通过以下方法:

func Is(err, target error) bool

接口定义

Is(target error) bool

使用示例

func ExampleIs() {
	err1 := errors.New("permission denied")
	err2 := gerror.Wrap(err1, "operation failed")
	fmt.Println(gerror.Is(err1, err1))
	fmt.Println(gerror.Is(err2, err2))
	fmt.Println(gerror.Is(err2, err1))
	fmt.Println(gerror.Is(err1, err2))

	// Output:
	// false
	// true
	// true
	// false
}

错误码特性

错误码使用

创建带错误码的 error

NewCode/NewCodef
func NewCode(code gcode.Code, text ...string) error
func NewCodef(code gcode.Code, format string, args ...any) error
func ExampleNewCode() {
	err := gerror.NewCode(gcode.New(10000, "", nil), "My Error")
	fmt.Println(err.Error())
	fmt.Println(gerror.Code(err))

	// Output:
	// My Error
	// 10000
}

func ExampleNewCodef() {
	err := gerror.NewCodef(gcode.New(10000, "", nil), "It's %s", "My Error")
	fmt.Println(err.Error())
	fmt.Println(gerror.Code(err).Code())

	// Output:
	// It's My Error
	// 10000
}
WrapCode/WrapCodef
func WrapCode(code gcode.Code, err error, text ...string) error
func WrapCodef(code gcode.Code, err error, format string, args ...any) error
func ExampleWrapCode() {
	err1 := errors.New("permission denied")
	err2 := gerror.WrapCode(gcode.New(10000, "", nil), err1, "Custom Error")
	fmt.Println(err2.Error())
	fmt.Println(gerror.Code(err2).Code())

	// Output:
	// Custom Error: permission denied
	// 10000
}

func ExampleWrapCodef() {
	err1 := errors.New("permission denied")
	err2 := gerror.WrapCodef(gcode.New(10000, "", nil), err1, "It's %s", "Custom Error")
	fmt.Println(err2.Error())
	fmt.Println(gerror.Code(err2).Code())

	// Output:
	// It's Custom Error: permission denied
	// 10000
}
NewCodeSkip/NewCodeSkipf
func NewCodeSkip(code gcode.Code, skip int, text ...string) error
func NewCodeSkipf(code gcode.Code, skip int, format string, args ...any) error

获取 error 中的错误码接口

func Code(err error) gcode.Code

当给定的 error 参数不带有错误码信息时,该方法返回预定义的错误码 gcode.CodeNil

错误码接口

基本介绍

提供了默认的错误码组件 gcode,错误码使用接口化设计,以实现高扩展性。

接口定义

// Code 通用错误代码接口定义。
type Code interface {
    // Code 错误码。
    Code() int
    // Message 错误码简短信息。
    Message() string
    // Detail 错误码详细信息。
    Detail() any
}

默认实现

提供了默认实现 gcode.Code 的结构体,开发者可以直接通过 New/WithCode 方法创建错误码:

func New(code int, message string, detail any) Code
func WithCode(code Code, detail any) Code
func ExampleNew() {
	c := gcode.New(1, "custom error", "detailed description")
	fmt.Println(c.Code())
	fmt.Println(c.Message())
	fmt.Println(c.Detail())

	// Output:
	// 1
	// custom error
	// detailed description
}

如果开发者觉得默认实现 gcode.Code 的结构体不满足需求,可以自行定义,只需实现 gcode.Code 即可。

错误码扩展

当业务需要复杂的错误码定义时,我们推荐灵活使用错误码的Detail参数来扩展错误码功能。

我们来看个例子。

业务错误码

错误码定义
type BizCode struct {
	User User
	// ...
}

type User struct {
	Id   int
	Name string
	// ...
}
错误码使用

扩展错误码大多数场景下需要使用 WithCode 方法:

func WithCode(code Code, detail any) Code

因此上面我们的自定义扩展可以这么使用:

code := gcode.WithCode(gcode.CodeNotFound, BizCode{
	User: User{
		Id:   1,
		Name: "John",
	},
})
fmt.Println(code)

即在错误码中我们可以根据业务场景注入一些自定义的错误码扩展数据,以方便上层获取错误码后做进一步处理。

错误码实现

当业务需要更复杂的错误码定义时,我们可以自定义实现业务自己的错误码,只需要实现gcode.Code相关的接口即可。

我们来看个例子。

自定义错误码

定义结构体并实现 gcode.code 接口定义的方法

type BizCode struct {
	code    int
	message string
	detail  BizCodeDetail
}
type BizCodeDetail struct {
	Code     string
	HttpCode int
}

func (c BizCode) BizDetail() BizCodeDetail {
	return c.detail
}

func (c BizCode) Code() int {
	return c.code
}

func (c BizCode) Message() string {
	return c.message
}

func (c BizCode) Detail() interface{} {
	return c.detail
}

func New(httpCode int, code string, message string) gcode.Code {
	return BizCode{
		code:    0,
		message: message,
		detail: BizCodeDetail{
			Code:     code,
			HttpCode: httpCode,
		},
	}
}

定义业务错误码

var (
	CodeNil      = New(200, "OK", "")
	CodeNotFound = New(404, "Not Found", "Resource does not exist")
	CodeInternal = New(500, "Internal Error", "An error occurred internally")
	// ...
)

内置错误码

框架提供了常见的一些错误码定义,开发者可直接使用:

https://github.com/camry/g/blob/main/gerrors/gcode/gcode.go

其它特性

NewOption 自定义的错误创建

func NewOption(option Option) error
func ExampleNewOption() {
 	err := gerror.NewOption(gerror.Option{
		Text: "this feature is disabled in this storage",
		Code: gcode.CodeNotSupported,
	})
}

fmt 格式化

通过以上示例我们可以看到,通过 %+v 的打印格式可以打印出完整的堆栈信息,当然 gerror.Error 对象支持多种 fmt 格式:

格式符 输出内容
%v,%s 打印所有的层级错误信息,构成完成的字符串返回,多个层级使用 : 拼接。
%-v, %-s 打印当前层级的错误信息,返回字符串。
%+s 打印完整的堆栈信息列表。
%+v 打印所有的层级错误信息字符串,以及完整的堆栈信息,等同于 %s\n%+s

使用示例:

package main

import (
    "errors"
    "fmt"

    "github.com/camry/g/gerrors/gerror"
)

func main() {
    var err error
    err = errors.New("sql error")
    err = gerror.Wrap(err, "adding failed")
    err = gerror.Wrap(err, "api calling failed")
    fmt.Printf(" %%s: %s\n", err)
    fmt.Printf("%%-s: %-s\n", err)
    fmt.Println("%+s: ")
    fmt.Printf("%+s\n", err)
}

// Output:
//  %s: api calling failed: adding failed: sql error
// %-s: api calling failed
// %+s:
// 1. api calling failed
//    1).  main.main
//         E:/project/Weition/go-example/main.go:14
// 2. adding failed
//    1).  main.main
//         E:/project/Weition/go-example/main.go:13
// 3. sql error

日志输出支持

glog 日志管理模块天然支持对 gerror 错误堆栈打印支持,这种支持不是强耦合性的,而是通过 fmt 格式化打印接口支持的。

使用示例:

package main

import (
    "errors"
    "github.com/camry/g/gerrors/gerror"
    "github.com/camry/g/glog"
)

func main() {
    var err error
    err = errors.New("sql error")
    err = gerror.Wrap(err, "adding failed")
    err = gerror.Wrap(err, "api calling failed")
    glog.Errorf("%+v", err)
}

// Output:
// ERROR msg=api calling failed: adding failed: sql error
// 1. api calling failed
//    1).  main.main
//         E:/project/Weition/go-example/main.go:13
// 2. adding failed
//    1).  main.main
//         E:/project/Weition/go-example/main.go:12
// 3. sql error

性能测试

常用方法的基准性能测试:https://github.com/camry/g/blob/main/gerrors/gerror/gerror_z_bench_test.go

goos: windows
goarch: amd64
pkg: github.com/camry/g/gerrors/gerror
cpu: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
Benchmark_New
Benchmark_New-12                 1722652               696.1 ns/op
Benchmark_Newf
Benchmark_Newf-12                1352446               877.1 ns/op
Benchmark_Wrap
Benchmark_Wrap-12                1471184               816.5 ns/op
Benchmark_Wrapf
Benchmark_Wrapf-12               1323144               894.9 ns/op
Benchmark_NewSkip
Benchmark_NewSkip-12             1754803               686.0 ns/op
Benchmark_NewSkipf
Benchmark_NewSkipf-12            1373864               872.4 ns/op
Benchmark_NewCode
Benchmark_NewCode-12             1431099               828.0 ns/op
Benchmark_NewCodef
Benchmark_NewCodef-12            1301948               919.9 ns/op
Benchmark_NewCodeSkip
Benchmark_NewCodeSkip-12         1402771               859.0 ns/op
Benchmark_NewCodeSkipf
Benchmark_NewCodeSkipf-12        1293831               918.0 ns/op
Benchmark_WrapCode
Benchmark_WrapCode-12            1402704               856.2 ns/op
Benchmark_WrapCodef
Benchmark_WrapCodef-12           1327546               902.1 ns/op
PASS