xerr

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2022 License: MIT Imports: 9 Imported by: 4

README

Xerr

Build Status License Coverage Status Go Reference


Package xerr provides an error with stack trace, a multi-error and other different types of errors.

Features:
  • an error enriched with stack trace
  • a MultiError
Error with stack trace

Basic example:

// create a new error with stack trace and print it.
err := xerr.New("something went bad")
fmt.Printf("%+v", err)

Output example:

something went bad
github.com/actforgood/xerr/_example/pkgb.OperationB
    /Users/bogdan/work/go/xerr/_example/pkgb/otherfile.go:7
github.com/actforgood/xerr/_example/pkga.OperationA
    /Users/bogdan/work/go/xerr/_example/pkga/somefile.go:6
main.main
    /Users/bogdan/work/go/xerr/_example/main.go:14
runtime.main
    /usr/local/go/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1371

Wrap another error example:

err := DoSomeOperation()
if err != nil {
    err = xerr.Wrap(err, "could not perform operation")
}

Output example:

could not perform operation: op err
github.com/actforgood/xerr/_example/pkgb.OperationB
    /Users/bogdan/work/go/xerr/_example/pkgb/otherfile.go:14
github.com/actforgood/xerr/_example/pkga.OperationA
    /Users/bogdan/work/go/xerr/_example/pkga/somefile.go:6
main.main
    /Users/bogdan/work/go/xerr/_example/main.go:14
runtime.main
    /usr/local/go/src/runtime/proc.go:225
runtime.goexit
    /usr/local/go/src/runtime/asm_amd64.s:1371
Shrinking the size of your error's output

You can reduce the I/O bytes and/or storage for your (logged) errors by shrinking the output of stack traces.
The package provides ways of manipulating the function name and excluding frames from the stack trace.

  • Example of excluding frames like /usr/local/go/src/ (which is my GOROOT src path):
// somewhere in your application bootstrap:
func init() {
    xerr.SetSkipFrame(xerr.SkipFrameGoRootSrcPath(xerr.AllowFrame))
}

Let's see how error's output looks like now:

something went bad
github.com/actforgood/xerr/_example/pkgb.OperationB
    /Users/bogdan/work/go/xerr/_example/pkgb/otherfile.go:11
github.com/actforgood/xerr/_example/pkga.OperationA
    /Users/bogdan/work/go/xerr/_example/pkga/somefile.go:6
main.main
    /Users/bogdan/work/go/xerr/_example/main.go:15

You can implement other rules of exclusion by yourself, and even chain multiple rules. Check SkipFrame and SkipFrameChain.

  • Example of saving some bytes by shorting the function name.
// somewhere in your application bootstrap:
func init() {
    xerr.SetFrameFnNameProcessor(xerr.ShortFunctionName)
}

Let's see how error's output looks like now:

something went bad
pkgb.OperationB
    /Users/bogdan/work/go/xerr/_example/pkgb/otherfile.go:11
pkga.OperationA
    /Users/bogdan/work/go/xerr/_example/pkga/somefile.go:6
main.main
    /Users/bogdan/work/go/xerr/_example/main.go:16

Check also other function names shrinkers: OnlyFunctionName, NoDomainFunctionName.

  • Tip: you can also shrink the filenames. This is not covered by this pkg, but you can achieve it by a go build/run flag. You can read this article.
go run -gcflags "all=-trimpath=/Users/bogdan/work/go" /Users/bogdan/work/go/xerr/_example/main.go
something went bad
pkgb.OperationB
    xerr/_example/pkgb/otherfile.go:11
pkga.OperationA
    xerr/_example/pkga/somefile.go:6
main.main
    xerr/_example/main.go:16
MultiError

You can collect multiple errors into a MultiError which implements error interface.
Basic sequential example:

files := []string{
    "/this/file/does/not/exist/1",
    "/this/file/does/not/exist/2",
    "/this/file/does/not/exist/3",
}
var multiErr *xerr.MultiError // save allocation if Add is never called.
for _, file := range files {
    if _, err := os.Open(file); err != nil {
        multiErr = multiErr.Add(err) // store allocated multiErr.
    }
    // else do something with that file ...
}

err := multiErr.ErrOrNil()
fmt.Println(err)
return err

Output example:

error #1
open /this/file/does/not/exist/1: no such file or directory
error #2
open /this/file/does/not/exist/2: no such file or directory
error #3
open /this/file/does/not/exist/3: no such file or directory

Basic parallel example:

files := []string{
	"/this/file/does/not/exist/1",
	"/this/file/does/not/exist/2",
	"/this/file/does/not/exist/3",
}
multiErr := xerr.NewMultiError() // we need instance already initialized.
var wg sync.WaitGroup
for _, file := range files {
	wg.Add(1)
	go func(filePath string, mErr *xerr.MultiError, waitGr *sync.WaitGroup) {
		defer waitGr.Done()
		if _, err := os.Open(filePath); err != nil {
			_ = mErr.Add(err) // we can dismiss returned value as multiErr is already initialized.
		}
		// else do something with that file ...
	}(file, multiErr, &wg)
}
wg.Wait()

returnErr := multiErr.ErrOrNil()
fmt.Println(returnErr)

Output example:

error #1
open /this/file/does/not/exist/3: no such file or directory
error #2
open /this/file/does/not/exist/1: no such file or directory
error #3
open /this/file/does/not/exist/2: no such file or directory
Misc

Feel free to use this pkg if you like it and fits your needs.
Check also other stack aware errors packages like pkg\errors, go-errors\errors.
For multi-error there are hashicorp\go-multierror, uber-go\multierr.
Here stands some benchmarks made locally for stacked error (note though each package err output may be different):

goos: darwin
goarch: amd64
pkg: github.com/actforgood/xerr
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkNewPkgErrors-8          1409167              4396 ns/op             680 B/op         11 allocs/op
BenchmarkNewGoErrors-8           48172              126978 ns/op           21255 B/op         71 allocs/op
BenchmarkNewXerr-8               2409250              2495 ns/op             656 B/op          7 allocs/op
// code snippet for other packages bench
import (
    "fmt"
    "testing"

    goErr "github.com/go-errors/errors"
    pkgErr "github.com/pkg/errors"
)

func BenchmarkNewPkgErrors(b *testing.B) {
    for n := 0; n < b.N; n++ {
        err := pkgErr.New("some error with stack trace")
        _ = fmt.Sprintf("%+v", err)
    }
}

func BenchmarkNewGoErrors(b *testing.B) {
    for n := 0; n < b.N; n++ {
        err := goErr.New("some error with stack trace")
        _ = err.ErrorStack()
    }
}
License

This package is released under a MIT license. See LICENSE.

Documentation

Overview

Package xerr provides different error functionalities, like an error enriched with stack trace, and a MultiError.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AllowFrame

func AllowFrame(_, _ string) bool

AllowFrame is a SkipFrame which whitelists any given frame. It can be used as the default/first SkipFrame in a chained responsibility configuration.

Example:

xerr.SetSkipFrame(SkipFoo(SkipBar(xerr.AllowFrame)))

func Errorf

func Errorf(format string, args ...interface{}) error

Errorf formats according to a format specifier and returns the string as a value that satisfies error. Errorf also records the stack trace at the point it was called.

Example
package main

import (
	"fmt"

	"github.com/actforgood/xerr"
)

func main() {
	err := xerr.Errorf("something went bad with %s", "this example")

	fmt.Println(err.Error())
	// Example output:
	// something went bad with this example

	fmt.Printf("%+v\n", err)
	// Example output:
	// something went bad with this example
	// github.com/actforgood/xerr_test.ExampleErrorf
	// 		/Users/bogdan/work/go/xerr/example_test.go:44
	// testing.runExample
	// 		/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	//		/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	//		/usr/local/go/src/testing/testing.go:1418
	// main.main
	//		_testmain.go:83
	// runtime.main
	// 		/usr/local/go/src/runtime/proc.go:225
	// runtime.goexit
	// 		/usr/local/go/src/runtime/asm_amd64.s:1371
}

func New

func New(msg string) error

New returns an error with the supplied message. New also records the stack trace at the point it was called.

Example
package main

import (
	"fmt"

	"github.com/actforgood/xerr"
)

func main() {
	err := xerr.New("something went bad")

	fmt.Println(err.Error())
	// Example output:
	// something went bad

	fmt.Printf("%+v\n", err)
	// Example output:
	// something went bad
	// github.com/actforgood/xerr_test.ExampleNew
	// 		/Users/bogdan/work/go/xerr/example_test.go:15
	// testing.runExample
	// 		/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	//		/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	//		/usr/local/go/src/testing/testing.go:1418
	// main.main
	//		_testmain.go:79
	// runtime.main
	// 		/usr/local/go/src/runtime/proc.go:225
	// runtime.goexit
	// 		/usr/local/go/src/runtime/asm_amd64.s:1371
}

func NoDomainFunctionName

func NoDomainFunctionName(fnName string) string

NoDomainFunctionName is a FrameFnNameProcessor which removes the first part (which is usually a domain) from fully qualified package name. Example: "github.com/actforgood/xerr_test.TestX" => "actforgood/xerr_test.TestX" .

func OnlyFunctionName

func OnlyFunctionName(fnName string) string

OnlyFunctionName is a FrameFnNameProcessor which returns only the function name, removing the package part. Example: "github.com/actforgood/xerr_test.TestX" => "TestX" .

func SetFrameFnNameProcessor

func SetFrameFnNameProcessor(fn FrameFnNameProcessor)

SetFrameFnNameProcessor configures the function this package uses in order to manipulate the function name from a stack trace frame. You will call it usually somewhere in the bootstrap process of your application. For example:

// myapp/bootstrap.go
func init() {
	xerr.SetFrameFnNameProcessor(xerr.ShortFunctionName)
}

func SetSkipFrame

func SetSkipFrame(fn SkipFrame)

SetSkipFrame configures the function this package uses in order to include/exclude frames from a stack trace of an error. You will call it usually somewhere in the bootstrap process of your application. For example:

// myapp/bootstrap.go
func init() {
	xerr.SetSkipFrame(SkipFoo(SkipBar(xerr.AllowFrame)))
}

func ShortFunctionName

func ShortFunctionName(fnName string) string

ShortFunctionName is a FrameFnNameProcessor which returns only the <package.funcName>, removing the fully qualified package name parts. Example: "github.com/actforgood/xerr_test.TestX" => "xerr_test.TestX" .

func Wrap

func Wrap(err error, msg string) error

Wrap returns an error annotating err with a stack trace at the point Wrap is called, and the supplied message. If err is nil, Wrap returns nil. If err is another stack trace aware error, the final stack trace will consists of original error's stack trace + 1 trace of current Wrap call.

Example (WithStackError)
package main

import (
	"fmt"

	"github.com/actforgood/xerr"
)

// DoSomeOtherOperation simulates a function which (may) return an error.
func DoSomeOtherOperation() error {
	return xerr.New("op err")
}

func main() {
	err := DoSomeOtherOperation()
	if err != nil {
		err = xerr.Wrap(err, "could not perform operation")
	}

	fmt.Println(err.Error())
	// Example output:
	// could not perform operation: op err

	fmt.Printf("%+v\n", err)
	// Example output:
	// could not perform operation: op err
	// github.com/actforgood/xerr_test.ExampleWrap_withStackError
	//         /Users/bogdan/work/go/xerr/example_test.go:83
	// github.com/actforgood/xerr_test.DoSomeOtherOperation
	//         /Users/bogdan/work/go/xerr/example_test.go:48
	// github.com/actforgood/xerr_test.ExampleWrap_withStackError
	//         /Users/bogdan/work/go/xerr/example_test.go:81
	// testing.runExample
	//         /usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	//         /usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	//         /usr/local/go/src/testing/testing.go:1418
	// main.main
	//         _testmain.go:79
	// runtime.main
	//         /usr/local/go/src/runtime/proc.go:225
	// runtime.goexit
	//         /usr/local/go/src/runtime/asm_amd64.s:1371
}
Example (WithStandardError)
package main

import (
	"errors"
	"fmt"

	"github.com/actforgood/xerr"
)

// DoSomeOperation simulates a function which (may) return an error.
func DoSomeOperation() error {
	return errors.New("op err")
}

func main() {
	err := DoSomeOperation()
	if err != nil {
		err = xerr.Wrap(err, "could not perform operation")
	}

	fmt.Println(err.Error())
	// Example output:
	// could not perform operation: op err

	fmt.Printf("%+v\n", err)
	// Example output:
	// could not perform operation: op err
	// github.com/actforgood/xerr_test.ExampleWrap_withStandardError
	// 		/Users/bogdan/work/go/xerr/example_test.go:54
	// testing.runExample
	// 		/usr/local/go/src/testing/run_example.go:63
	// testing.runExamples
	//		/usr/local/go/src/testing/example.go:44
	// testing.(*M).Run
	//		/usr/local/go/src/testing/testing.go:1418
	// main.main
	//		_testmain.go:81
	// runtime.main
	// 		/usr/local/go/src/runtime/proc.go:225
	// runtime.goexit
	// 		/usr/local/go/src/runtime/asm_amd64.s:1371
}

func Wrapf

func Wrapf(err error, format string, args ...interface{}) error

Wrapf returns an error annotating err with a stack trace at the point Wrap is called, and the message formatted according to a format specifier. If err is nil, Wrap returns nil. If err is another stack trace aware error, the final stack trace will consists of original error's stack trace + 1 trace of current Wrapf call.

Types

type FrameFnNameProcessor

type FrameFnNameProcessor func(fnName string) string

FrameFnNameProcessor is an alias for a function that can manipulate the function name from a stack trace frame. You can apply customizations upon function name output this way.

type MultiError

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

MultiError holds a pool of errors. Its APIs are concurrent safe if you initialize it with NewMultiError().

func NewMultiError

func NewMultiError() *MultiError

NewMultiError instantiates a new MultiError object. Use it to initialize from start your MultiError variable if you use it in a concurrent context, or you need to pass it as parameter to a function. Otherwise just declare the variable's type and get effective instance returned by Add() / AddOnce() APIs, to avoid unnecessary allocation if those APIs end up never being called.

func (*MultiError) Add

func (mErr *MultiError) Add(errs ...error) *MultiError

Add appends the given error(s) in MultiError. It returns the MultiError, eventually initialized.

Example (Parallel)
package main

import (
	"fmt"
	"os"
	"sync"

	"github.com/actforgood/xerr"
)

func main() {
	files := []string{
		"/this/file/does/not/exist/1",
		"/this/file/does/not/exist/2",
		"/this/file/does/not/exist/3",
	}
	multiErr := xerr.NewMultiError() // we need instance already initialized.
	var wg sync.WaitGroup
	for _, file := range files {
		wg.Add(1)
		go func(filePath string, mErr *xerr.MultiError, waitGr *sync.WaitGroup) {
			defer waitGr.Done()
			if _, err := os.Open(filePath); err != nil {
				_ = mErr.Add(err) // we can dismiss returned value as multiErr is already initialized.
			}
			// else do something with that file ...
		}(file, multiErr, &wg)
	}
	wg.Wait()

	returnErr := multiErr.ErrOrNil()
	fmt.Println(returnErr)

	// Example of unordered output:
	// error #1
	// open /this/file/does/not/exist/3: no such file or directory
	// error #2
	// open /this/file/does/not/exist/1: no such file or directory
	// error #3
	// open /this/file/does/not/exist/2: no such file or directory
}
Example (Sequential)
package main

import (
	"fmt"
	"os"

	"github.com/actforgood/xerr"
)

func main() {
	files := []string{
		"/this/file/does/not/exist/1",
		"/this/file/does/not/exist/2",
		"/this/file/does/not/exist/3",
	}
	var multiErr *xerr.MultiError // save allocation if Add is never called.
	for _, file := range files {
		if _, err := os.Open(file); err != nil {
			multiErr = multiErr.Add(err) // store allocated multiErr.
		}
		// else do something with that file ...
	}

	returnErr := multiErr.ErrOrNil()
	fmt.Println(returnErr)

	// Example of output: note - on Windows the message is slightly different (The system cannot find the path specified)
	// error #1
	// open /this/file/does/not/exist/1: no such file or directory
	// error #2
	// open /this/file/does/not/exist/2: no such file or directory
	// error #3
	// open /this/file/does/not/exist/3: no such file or directory
}

func (*MultiError) AddOnce

func (mErr *MultiError) AddOnce(errs ...error) *MultiError

AddOnce stores the given error(s) in MultiError, only if they do not exist already. Comparison is accomplished with Is() API. It returns the MultiError, eventually initialized.

Example
package main

import (
	"fmt"
	"io"
	"os"

	"github.com/actforgood/xerr"
)

func main() {
	// different errors we want to add to MultiError
	err1 := os.ErrNotExist
	err2 := io.ErrUnexpectedEOF
	err3 := os.ErrNotExist

	var multiErr *xerr.MultiError
	multiErr = multiErr.AddOnce(err1)
	multiErr = multiErr.AddOnce(err2)
	multiErr = multiErr.AddOnce(err3) // err3 is the same with err1, so it should be ignored

	returnErr := multiErr.ErrOrNil()
	fmt.Println(returnErr)

}
Output:

error #1
file does not exist
error #2
unexpected EOF

func (*MultiError) As

func (mErr *MultiError) As(target interface{}) bool

As implements standard error As() API, comparing the first error from stored ones.

func (*MultiError) ErrOrNil

func (mErr *MultiError) ErrOrNil() error

ErrOrNil returns nil if MultiError does not have any stored errors, or the single error it stores, or self if has more more than 1 error.

func (*MultiError) Error

func (mErr *MultiError) Error() string

Error returns the error's message. Implements std error interface. Returns all stored errors' messages, new line separated.

func (*MultiError) Errors

func (mErr *MultiError) Errors() []error

Errors returns a copy of stored errors.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/actforgood/xerr"
)

func main() {
	var multiErr = xerr.NewMultiError()
	_ = multiErr.Add(errors.New("1st error"))
	_ = multiErr.Add(errors.New("2nd error"))

	for _, err := range multiErr.Errors() {
		fmt.Println(err)
	}

}
Output:

1st error
2nd error

func (*MultiError) Format

func (mErr *MultiError) Format(f fmt.State, verb rune)

Format implements fmt.Formatter. It relies upon individual error's Format() API if applicable, otherwise Error() 's outcome is taken into account.

func (*MultiError) Is

func (mErr *MultiError) Is(target error) bool

Is implements standard error Is() API, comparing the first error from stored ones.

Example
package main

import (
	"errors"
	"fmt"
	"io"
	"os"

	"github.com/actforgood/xerr"
)

func main() {
	var multiErr = xerr.NewMultiError()
	_ = multiErr.Add(io.ErrUnexpectedEOF)
	someErrWithStack := xerr.New("stack err")
	_ = multiErr.Add(someErrWithStack)

	fmt.Println(errors.Is(multiErr, io.ErrUnexpectedEOF))
	fmt.Println(errors.Is(multiErr, someErrWithStack))
	fmt.Println(errors.Is(multiErr, os.ErrClosed))

}
Output:

true
true
false

func (*MultiError) Reset

func (mErr *MultiError) Reset()

Reset cleans up stored errors, if any.

func (*MultiError) Unwrap

func (mErr *MultiError) Unwrap() error

Unwrap returns original error (can be nil). It implements standard error Is()/As() APIs. Returns recursively first error from stored errors.

type SkipFrame

type SkipFrame func(fnName, file string) bool

SkipFrame is alias for a function that decides whether a frame should be included in the stack trace or not.

func SkipFrameGoRootSrcPath

func SkipFrameGoRootSrcPath(next SkipFrame) SkipFrame

SkipFrameGoRootSrcPath is a chained function which blacklists frames with files starting with "GOROOT/src" path.

type SkipFrameChain

type SkipFrameChain func(next SkipFrame) SkipFrame

SkipFrameChain is a alias for a chained SkipFrame.

Jump to

Keyboard shortcuts

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