206 lines
6.4 KiB
Go
206 lines
6.4 KiB
Go
|
// Copyright (C) 2014 Space Monkey, Inc.
|
||
|
//
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
|
||
|
/*
|
||
|
Package errors is a flexible error support library for Go
|
||
|
|
||
|
Motivation
|
||
|
|
||
|
Go's standard library is intentionally sparse on providing error utilities, and
|
||
|
developers coming from other programming languages may miss some features they
|
||
|
took for granted [1]. This package is an attempt at providing those features in
|
||
|
an idiomatic Go way.
|
||
|
|
||
|
The main features this package provides (in addition to miscellaneous
|
||
|
utilities) are:
|
||
|
|
||
|
* Error hierarchies
|
||
|
* Stack traces
|
||
|
* Arbitrary error values
|
||
|
|
||
|
Error hierarchies
|
||
|
|
||
|
While Go has very deliberately not implemented class hierarchies, a quick
|
||
|
perusal of Go's net and os packages should indicate that sometimes error
|
||
|
hierarchies are useful. Go programmers should be familiar with the net.Error
|
||
|
interface (and the types that fulfill it) as well as the os helper functions
|
||
|
such as os.IsNotExist, os.IsPermission, etc.
|
||
|
|
||
|
Unfortunately, to implement something similar, a developer will have to
|
||
|
implement a struct that matches the error interface as well as any testing
|
||
|
methods or any more detailed interfaces they may choose to export. It's not
|
||
|
hard, but it is friction, and developers tend to use fmt.Errorf instead due
|
||
|
to ease of use, thus missing out on useful features that functions like
|
||
|
os.IsNotExist and friends provide.
|
||
|
|
||
|
The errors package provides reusable components for building similar
|
||
|
features while reducing friction as much as possible. With the errors package,
|
||
|
the os error handling routines can be mimicked as follows:
|
||
|
|
||
|
package osmimic
|
||
|
|
||
|
import (
|
||
|
"github.com/spacemonkeygo/errors"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
OSError = errors.NewClass("OS Error")
|
||
|
NotExist = OSError.NewClass("Not Exist")
|
||
|
)
|
||
|
|
||
|
func Open(path string) (*File, error) {
|
||
|
// actually do something here
|
||
|
return nil, NotExist.New("path %#v doesn't exist", path)
|
||
|
}
|
||
|
|
||
|
func MyMethod() error {
|
||
|
fh, err := Open(mypath)
|
||
|
if err != nil {
|
||
|
if NotExist.Contains(err) {
|
||
|
// file doesn't exist, do stuff
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
// do stuff
|
||
|
}
|
||
|
|
||
|
Stack traces
|
||
|
|
||
|
It doesn't take long during Go development before you may find yourself
|
||
|
wondering where an error came from. In other languages, as soon as an error is
|
||
|
raised, a stack trace is captured and is displayed as part of the language's
|
||
|
error handling. Go error types are simply basic values and no such magic
|
||
|
happens to tell you what line or what stack an error came from.
|
||
|
|
||
|
The errors package fixes this by optionally (but by default) capturing the
|
||
|
stack trace as part of your error. This behavior can be turned off and on for
|
||
|
specific error classes and comes in two flavors. You can have the stack trace
|
||
|
be appended to the error's Error() message, or you can have the stack trace
|
||
|
be logged immediately, every time an error of that type is instantiated.
|
||
|
|
||
|
Every error and error class supports hierarchical settings, in the sense that
|
||
|
if a setting was not explicitly set on that error or error class, setting
|
||
|
resolution traverses the error class hierarchy until it finds a valid setting,
|
||
|
or returns the default.
|
||
|
|
||
|
See CaptureStack()/NoCaptureStack() and LogOnCreation()/NoLogOnCreation() for
|
||
|
how to control this feature.
|
||
|
|
||
|
Arbitrary error values
|
||
|
|
||
|
These hierarchical settings (for whether or not errors captured or logged stack
|
||
|
traces) were so useful, we generalized the system to allow users to extend
|
||
|
the errors system with their own values. A user can tag a specific error with
|
||
|
some value given a statically defined key, or tag a whole error class subtree.
|
||
|
|
||
|
Arbitrary error values can easily handle situtations like net.Error's
|
||
|
Temporary() field, where some errors are temporary and others aren't. This can
|
||
|
be mimicked as follows:
|
||
|
|
||
|
package netmimic
|
||
|
|
||
|
import (
|
||
|
"github.com/spacemonkeygo/errors"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
NetError = errors.NewClass("Net Error")
|
||
|
OpError = NetError.NewClass("Op Error")
|
||
|
|
||
|
tempErrorKey = errors.GenSym()
|
||
|
)
|
||
|
|
||
|
func SetIsTemporary() errors.ErrorOption {
|
||
|
return errors.SetData(tempErrorKey, true)
|
||
|
}
|
||
|
|
||
|
func IsTemporary(err error) bool {
|
||
|
v, ok := errors.GetData(err, tempErrorKey).(bool)
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
return v
|
||
|
}
|
||
|
|
||
|
func NetworkOp() error {
|
||
|
// actually do something here
|
||
|
return OpError.NewWith("failed operation", SetIsTemporary())
|
||
|
}
|
||
|
|
||
|
func Example() error {
|
||
|
for {
|
||
|
err := NetworkOp()
|
||
|
if err != nil {
|
||
|
if IsTemporary(err) {
|
||
|
// probably should do exponential backoff
|
||
|
continue
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
HTTP handling
|
||
|
|
||
|
Another great example of arbitrary error value functionality is the errhttp
|
||
|
subpackage. See the errhttp source for more examples of how to use
|
||
|
SetData/GetData.
|
||
|
|
||
|
The errhttp package really helped clean up our error code. Take a look to
|
||
|
see if it can help your error handling with HTTP stacks too.
|
||
|
|
||
|
http://godoc.org/github.com/spacemonkeygo/errors/errhttp
|
||
|
|
||
|
Exit recording
|
||
|
|
||
|
So you have stack traces, which tells you how the error was generated, but
|
||
|
perhaps you're interested in keeping track of how the error was handled?
|
||
|
|
||
|
Every time you call errors.Record(err), it adds the current line information
|
||
|
to the error's output. As an example:
|
||
|
|
||
|
func MyFunction() error {
|
||
|
err := Something()
|
||
|
if err != nil {
|
||
|
if IsTemporary(err) {
|
||
|
// manage the temporary error
|
||
|
return errors.Record(err)
|
||
|
} else {
|
||
|
// manage the permanent error
|
||
|
return errors.Record(err)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
errors.Record will help you keep track of which error handling branch your
|
||
|
code took.
|
||
|
|
||
|
ErrorGroup
|
||
|
|
||
|
There's a few different types of ErrorGroup utilities in this package, but they
|
||
|
all work the same way. Make sure to check out the ErrorGroup example.
|
||
|
|
||
|
CatchPanic
|
||
|
|
||
|
CatchPanic helps you easily manage functions that you think might panic, and
|
||
|
instead return errors. CatchPanic works by taking a pointer to your named error
|
||
|
return value. Check out the CatchPanic example for more.
|
||
|
|
||
|
Footnotes
|
||
|
|
||
|
[1] This errors package started while porting a large Python codebase to Go.
|
||
|
https://www.spacemonkey.com/blog/posts/go-space-monkey
|
||
|
*/
|
||
|
package errors
|