goerr

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Dec 12, 2019 License: MIT Imports: 6 Imported by: 5

README

goerr

GoReport GoLang GoDoc License

This package attempts to bring proposed error handling in Go v2 into Go v1. see: https://go.googlesource.com/proposal/+/master/design/go2draft.md

Usage

go get -u github.com/brad-jones/goerr

package copy

import (
    "os"
    "fmt"
    "io"

    . "github.com/brad-jones/goerr"
    "github.com/go-errors/errors"
)


func File(src, dst string) (err error) {
    defer Handle(func(e error){
        err = errors.Errorf("copy %s %s: %v", src, dst, e)
    })

    r, err := os.Open(src); Check(err)
    defer r.Close()

    w, err := os.Create(dst); Check(err)
    defer Handle(func(e error){
        w.Close()
        os.Remove(dst)
        panic(e)
    })

    _, err = io.Copy(w, r); Check(err)
    Check(w.Close())

    return nil
}

Documentation

Overview

Package goerr attempts to bring proposed error handling in Go v2 into Go v1.

see: https://go.googlesource.com/proposal/+/master/design/go2draft.md

Preface: My ideas presented here may not necessarily be theoretically perfect but I am taking a pragmatic approach to my development of golang code, while at the same time keeping in mind that go is go and not another language... go is verbose suck it up and move on.

Check and Handle

We can emulate the proposed `check` and `handle` using `panic, defer & recover`. Take the same example from the proposal that has been refactored to use goerr.

import . "github.com/brad-jones/goerr"

func CopyFile(src, dst string) (err error) {
	defer Handle(func(e error){
		err = fmt.Errorf("copy %s %s: %v", src, dst, e)
	})

	r, err := os.Open(src); Check(err)
	defer r.Close()

	w, err := os.Create(dst); Check(err)
	defer Handle(func(e error){
		w.Close()
		os.Remove(dst)
		panic(e) // re-panic to make above handler set the err
	})

	_, err = io.Copy(w, r); Check(err)
	Check(w.Close())

	return nil
}

So `Check()` replaces the repetitive `if err != nil { ... }` phrase and `Handle` takes care of the `recover()` logic for you.

Idiomatic Must Functions

It is idiomatic in golang for functions that might panic to be prefixed with `Must`. I am going to take things a step further though and suggest that life could be much easier if every (or most) function also had a `MustFoo()` equivalent.

Adding the extra wrapping function is minimal effort for the API developer (5 lines) but now gives the API consumer ultimate choice in how they want to deal with errors.

Another example that eliminates the `Check` logic:

import . "github.com/brad-jones/goerr"

func CopyFile(src, dst string) (err error) {
	defer Handle(func(e error){
		err = fmt.Errorf("copy %s %s: %v", src, dst, e)
	})

	r := os.MustOpen(src);
	defer r.MustClose()

	w := os.MustCreate(dst);
	defer Handle(func(e error){
		w.MustClose()
		os.MustRemove(dst)
		panic(e) // re-panic to make above handler set the err
	})

	io.MustCopy(w, r)
	w.MustClose()

	return nil
}

Oh No Exceptions

This looks awefully like exceptions that the go designers are purposefully avoiding. We could go one step further with our example:

import . "github.com/brad-jones/goerr"

func MustCopyFile(src, dst string) {
	r := os.MustOpen(src);
	defer r.MustClose()

	w := os.MustCreate(dst);
	defer Handle(func(e error){
		w.MustClose()
		os.MustRemove(dst)
		panic(e)
	})

	io.MustCopy(w, r)
	w.MustClose()

	return nil
}

And this is where my pragmatic approach kicks in, firstly the above still communicates that it might panic so the consumer should be ready for such a possibility.

It is idiomatic go to not panic across package boundaries and for the most part I agree with this but I am going to extend this by saying that you should not panic across solution boundaries.

Panicking with-in a solution or application (that could be split into many packages) I believe is fine as you know when your going to panic and when you need to recover.

Other Thoughts re Panicking

I am sure the performance of panic, defer & recover is slower than just checking and returning an error value. Unless I am writing some super duper performance oriented thing I doubt I'll notice any impacts.

Panicking across goroutines, yep I get it, it does not work. I am totally fine with this. There are packages like https://godoc.org/golang.org/x/sync/errgroup to handle such cases.

Also now: https://godoc.org/github.com/brad-jones/goasync

Recover doesn't always work https://go101.org/article/panic-and-recover-more.html This nearly made me drop this entire project but I am going to persevere and see how this package works out.

Wrapping of Errors

The other issue that will hopefully get solved in go v2 is the ability to provide context to errors as they get passed through the stack. For now we have solutions such as:

https://godoc.org/github.com/pkg/errors

https://godoc.org/github.com/go-errors/errors

This package provides some helpful functions to iron some of the differences between these error wrappers (I started using pkg/errors but now prefer go-errors/errors).

import "github.com/go-errors/errors"
import . "github.com/brad-jones/goerr"

type anError struct {
	message string
}

func (e *anError) Error() string {
	return e.message
}

func foo() error {
	return errors.New(&anError{
		message: "an error happened",
	})
}

func bar() error {
	return errors.WrapPrefix(foo(), "some extra context", 0)
}

func main() {
	defer Handle(func(e error){
		switch err := MustUnwrap(e).(type) {
		case *anError:
			fmt.Println(err.message)
		default:
			fmt.Println(MustTrace(e))
		}
	})
	Check(bar())
}

Helper Functions vs New Instance

You can use goerr via 2 different APIs, all the examples to date have been using the simple helper functions but all these do is call the instance methods of a `goerr.Goerr` object.

If you need to set your own logger (and maybe other things in the future) this is how you would do it.

import logger "github.com/go-log/log/fmt"
import "github.com/brad-jones/goerr"

func main() {
	l := logger.New()
	g := goerr.New(l)
	defer g.HandleAndLog(func(e error){ })
}

see: https://github.com/go-log/log

NOTE: We have also been using a "dot" import this is not necessarily suggested either, if your not familiar with this read up here - https://scene-si.org/2018/01/25/go-tips-and-tricks-almost-everything-about-imports/

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Check

func Check(err error)

Check uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).Check()`.

Example
package main

import (
	"os"

	"github.com/brad-jones/goerr"
)

func main() {
	_, err := os.Open("")
	goerr.Check(err) // expect this to panic
}
Output:

func Errorf

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

Errorf uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).Errorf()`.

func Handle

func Handle(onError func(err error))

Handle uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).Handle()`.

Example
package main

import (
	"fmt"
	"os"

	"github.com/brad-jones/goerr"
)

func main() {
	defer goerr.Handle(func(err error) {
		fmt.Println("an error")
	})
	_, err := os.Open("")
	goerr.Check(err) // expect this to panic
}
Output:

an error

func HandleAndLog

func HandleAndLog(onError func(err error))

HandleAndLog uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).HandleAndLog()`.

Example
package main

import (
	"fmt"
	"os"

	"github.com/brad-jones/goerr"
)

func main() {
	defer goerr.HandleAndLog(func(err error) {
		fmt.Println("an error")
	})
	_, err := os.Open("")
	goerr.Check(err) // expect this to panic
}
Output:

open : no such file or directory
an error

func HandleAndLogWithTrace

func HandleAndLogWithTrace(onError func(err error))

HandleAndLogWithTrace uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).HandleAndLogWithTrace()`.

Example
package main

import (
	"fmt"
	"os"

	"github.com/brad-jones/goerr"
	"github.com/go-errors/errors"
)

func main() {
	defer goerr.HandleAndLogWithTrace(func(err error) {
		fmt.Println("an error")
	})
	_, err := os.Open("")
	goerr.Check(errors.New(err)) // expect this to panic

	// open : no such file or directory
	// *os.PathError open : no such file or directory
	// /home/brad/Projects/Personal/goerr/examples_test.go:42 (0x4f2aea)
	// 	ExampleHandleAndLogWithTrace: goerr.Check(errors.New(err)) // expect this to panic
	// /home/brad/.goenv/versions/1.12.5/src/testing/example.go:121 (0x4b15ed)
	// 	runExample: eg.F()
	// /home/brad/.goenv/versions/1.12.5/src/testing/example.go:45 (0x4b1218)
	// 	runExamples: if !runExample(eg) {
	// /home/brad/.goenv/versions/1.12.5/src/testing/testing.go:1073 (0x4b4f6f)
	// 	(*M).Run: exampleRan, exampleOk := runExamples(m.deps.MatchString, m.examples)
	// _testmain.go:48 (0x4f2eee)
	// /home/brad/.goenv/versions/1.12.5/src/runtime/proc.go:200 (0x42ca6c)
	// 	main: fn()
	// /home/brad/.goenv/versions/1.12.5/src/runtime/asm_amd64.s:1337 (0x457881)
	// 	goexit: BYTE	$0x90	// NOP
	//
	// an error
}
Output:

func MustTrace

func MustTrace(err error) string

MustTrace uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).MustTrace()`.

func MustUnwrap

func MustUnwrap(err error) error

MustUnwrap uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).MustUnwrap()`.

func Trace

func Trace(err error) (string, error)

Trace uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).Trace()`.

Example
package main

import (
	"fmt"

	"github.com/brad-jones/goerr"
	"github.com/go-errors/errors"
)

func main() {
	st, _ := goerr.Trace(errors.New("an error"))
	fmt.Println(st)

	// *errors.errorString an error
	// /home/brad/Projects/Personal/goerr/examples_test.go:70 (0x4f2a9d)
	// 	ExampleTrace: st, _ := goerr.Trace(errors.New("an error"))
	// /home/brad/.goenv/versions/1.12.5/src/testing/example.go:121 (0x4b15ed)
	// 	runExample: eg.F()
	// /home/brad/.goenv/versions/1.12.5/src/testing/example.go:45 (0x4b1218)
	// 	runExamples: if !runExample(eg) {
	// /home/brad/.goenv/versions/1.12.5/src/testing/testing.go:1073 (0x4b4f6f)
	// 	(*M).Run: exampleRan, exampleOk := runExamples(m.deps.MatchString, m.examples)
	// _testmain.go:50 (0x4f2e7e)
	// /home/brad/.goenv/versions/1.12.5/src/runtime/proc.go:200 (0x42ca6c)
	// 	main: fn()
	// /home/brad/.goenv/versions/1.12.5/src/runtime/asm_amd64.s:1337 (0x457881)
	// 	goexit: BYTE	$0x90	// NOP
}
Output:

func Unwrap

func Unwrap(err error) (error, error)

Unwrap uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).Unwrap()`.

Example
package main

import (
	"fmt"

	"github.com/brad-jones/goerr"
	"github.com/go-errors/errors"
)

func main() {
	originalError := &goerr.ErrUnwrappingNotSupported{}
	wrappedError := errors.New(originalError)
	unWrappedError, _ := goerr.Unwrap(wrappedError)
	fmt.Println(unWrappedError == originalError)
}
Output:

true

func Wrap

func Wrap(err error) error

Wrap uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).Wrap()`.

func WrapPrefix

func WrapPrefix(err error, prefix string) error

WrapPrefix uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).WrapPrefix()`.

Types

type ErrStackTraceNotSupported

type ErrStackTraceNotSupported struct {
	OriginalError error
}

ErrStackTraceNotSupported is returned by Trace and panic'ed by MustTrace

func (*ErrStackTraceNotSupported) Error

func (e *ErrStackTraceNotSupported) Error() string

type ErrUnwrappingNotSupported

type ErrUnwrappingNotSupported struct {
	OriginalError error
}

ErrUnwrappingNotSupported is returned by Unwrap and panic'ed by MustUnwrap

func (*ErrUnwrappingNotSupported) Error

func (e *ErrUnwrappingNotSupported) Error() string

type Goerr

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

Goerr is a class like object, create new instances with `goerr.New()`

func New

func New(logger log.Logger) *Goerr

New creates new instances of `Goerr`.

The logger must implement the interface from https://github.com/go-log/log

func (*Goerr) Check

func (g *Goerr) Check(err error)

Check will panic if err is not null

func (*Goerr) Errorf

func (g *Goerr) Errorf(format string, a ...interface{}) error

Errorf is just an alias to "github.com/go-errors/errors".

func (*Goerr) Handle

func (g *Goerr) Handle(onError func(err error))

Handle will recover, cast the result into an error and then call the provided onError handler.

Goes without saying but for this to be useful you must preface it with `defer`.

func (*Goerr) HandleAndLog

func (g *Goerr) HandleAndLog(onError func(err error))

HandleAndLog does the same thing as Handle but also logs (using the provided logger) the error message.

func (*Goerr) HandleAndLogWithTrace

func (g *Goerr) HandleAndLogWithTrace(onError func(err error))

HandleAndLogWithTrace does the same thing as HandleAndLog but also logs (using the provided logger) a stack trace that was attached to the error.

If Trace returns an error nothing will be logged and it will silently fail. This would be the case if you are handling a non wrapped error.

func (*Goerr) MustTrace

func (g *Goerr) MustTrace(err error) string

MustTrace does the same thing as Trace but panics instead of returning an error.

func (*Goerr) MustUnwrap

func (g *Goerr) MustUnwrap(err error) error

MustUnwrap does the same thing as Unwrap but panics instead of returning a second error.

func (*Goerr) Trace

func (g *Goerr) Trace(err error) (string, error)

Trace takes an error value and assumes it is either a https://github.com/go-errors/errors object or a https://github.com/pkg/errors object and attempts to extract a stack trace from the error value, returning it as a string.

If the error does not appear to have stack trace attached, this will return a wrapped *ErrUnwrappingNotSupported object.

func (*Goerr) Unwrap

func (g *Goerr) Unwrap(err error) (error, error)

Unwrap takes an error value and assumes it is either a https://github.com/go-errors/errors object or a https://github.com/pkg/errors object and attempts to unwrap the error returning the original untouched error value.

If the error can not be unwrapped, the second error returned will be a wrapped *ErrUnwrappingNotSupported object.

func (*Goerr) Wrap

func (g *Goerr) Wrap(err error) error

Wrap is just an alias to "github.com/go-errors/errors".

func (*Goerr) WrapPrefix

func (g *Goerr) WrapPrefix(err error, prefix string) error

WrapPrefix is just an alias to "github.com/go-errors/errors".

Jump to

Keyboard shortcuts

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