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 ¶
- func Check(err error)
- func Errorf(format string, a ...interface{}) error
- func Handle(onError func(err error))
- func HandleAndLog(onError func(err error))
- func HandleAndLogWithTrace(onError func(err error))
- func MustTrace(err error) string
- func MustUnwrap(err error) error
- func Trace(err error) (string, error)
- func Unwrap(err error) (error, error)
- func Wrap(err error) error
- func WrapPrefix(err error, prefix string) error
- type ErrStackTraceNotSupported
- type ErrUnwrappingNotSupported
- type Goerr
- func (g *Goerr) Check(err error)
- func (g *Goerr) Errorf(format string, a ...interface{}) error
- func (g *Goerr) Handle(onError func(err error))
- func (g *Goerr) HandleAndLog(onError func(err error))
- func (g *Goerr) HandleAndLogWithTrace(onError func(err error))
- func (g *Goerr) MustTrace(err error) string
- func (g *Goerr) MustUnwrap(err error) error
- func (g *Goerr) Trace(err error) (string, error)
- func (g *Goerr) Unwrap(err error) (error, error)
- func (g *Goerr) Wrap(err error) error
- func (g *Goerr) WrapPrefix(err error, prefix string) error
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 ¶
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 ¶
MustTrace uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).MustTrace()`.
func MustUnwrap ¶
MustUnwrap uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).MustUnwrap()`.
func Trace ¶
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 ¶
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 ¶
Wrap uses the `defaultInstance` with a preconfigured logger. It does the same thing as `goerr.New(logger).Wrap()`.
func WrapPrefix ¶
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 ¶
New creates new instances of `Goerr`.
The logger must implement the interface from https://github.com/go-log/log
func (*Goerr) Handle ¶
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 ¶
HandleAndLog does the same thing as Handle but also logs (using the provided logger) the error message.
func (*Goerr) HandleAndLogWithTrace ¶
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 ¶
MustTrace does the same thing as Trace but panics instead of returning an error.
func (*Goerr) MustUnwrap ¶
MustUnwrap does the same thing as Unwrap but panics instead of returning a second error.
func (*Goerr) Trace ¶
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 ¶
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.