Skip to content

Commit

Permalink
Merge fe771eb into ef7c3c9
Browse files Browse the repository at this point in the history
  • Loading branch information
ifraixedes committed Jan 3, 2019
2 parents ef7c3c9 + fe771eb commit 99bdc50
Show file tree
Hide file tree
Showing 57 changed files with 10,418 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.bin
36 changes: 36 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
go_import_path: go.fraixed.es/errors
language: go

matrix:
include:
- go: "1.11.x"
env: GO111MODULE=on
env: LINT=true
- go: "1.10.x"
- go: "1.9.x"
- go: "1.8.x"
- go: tip
env: GO111MODULE=on
allow_failures:
- go: tip
env: GO111MODULE=on

before_install:
# setup some env vars
- GO_FILES=$(find . -iname '*.go' | grep -v /vendor/) # All the .go files, excluding vendor/
- PKGS=$(go list ./... | grep -v /vendor/) # All the import paths, excluding vendor/

# Install CI tools
- make .go-tools-install-ci

# Install goveralls, Go integration for Coveralls.io.
- go get -u github.com/mattn/goveralls

script:
- test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt
- if [ "$LINT" = true ]; then make lint; fi
- go test -v -covermode=count -coverprofile=profile.cov $PKGS
- goveralls -coverprofile=profile.cov -service=travis-ci

notifications:
email: false
30 changes: 30 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
GOLANGCI_LINT_VERSION := 1.12.3
GOLANGCI_LINT_BIN := $(realpath .bin/golangci-lint)

define HELP_MSG
Execute one of the following targets:

endef

export HELP_MSG


.PHONY: help
help: ## Show this help
@echo "$$HELP_MSG"
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/:.*##/:##/' | column -t -s '##'

.PHONY: go-tools-install
go-tools-install: .gti-golangci-lint ## Install Go tools

.PHONY: lint
lint: ## Lint the code
@$(GOLANGCI_LINT_BIN) run --enable-all --exclude-use-default=false

.PHONY: .go-tools-install-ci
.go-tools-install-ci: .gti-golangci-lint

.PHONY: .gti-golangci-lint
.gti-golangci-lint:
@mkdir -p .bin
@curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b .bin v$(GOLANGCI_LINT_VERSION)
103 changes: 100 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,107 @@
# Go Errors

This package provides a way to create and manage Go errors using codes using a simple and small API and without requiring type casting.
[![GoDoc](https://godoc.org/go.fraixed.es/errors?status.svg)](https://godoc.org/go.fraixed.es/errors)

__NOTE__ it's work in progress and at the time that it's ready for being used this README will be updated with more information about the package.
This package provides a simple API for creating and handling errors which are identified by codes.

The correct import path of this package is `go.fraixed.es/errors`.

## Rationale

Many times, a library, service, or any other kind of implementation needs to
deal with third party packages and it may need to identify errors for:

1. Perform some operation before returning it to the caller, for example doing
some kind of rollback operations.
2. Localize the error.

Meanwhile those 2 needs could be implemented with error values (defining them
as global variables of the package) or specific types, both of them aren't ideal
because:

1. Package variables values cannot be modified on each error instance, so they
cannot to be endowed with specific information which is useful for the
developers and operations teams for identifying the source of the problem.
2. Package variables aren't immutable, so their values, although unlikely, could
be erroneous overridden.
3. Specific types don't have the 2 previous problems, however when a considerable
list of different errors is needed, quite a few boilerplate is required for
creating them and the package documentation gets polluted.
4. Specific types must implement the same behavior over and over or use a base
type which must be embedded in all the types to have the same logic. This could
be fine in single package, but when you want to have the same mechanism on a
bunch of packages (think in medium/large implementations done by any company
whose code base is written in Go) is less than desirable to spread between
teams.
5. When using any of them and the errors are transmitted over the wire (this
problem arises when the dependency is a remote service), then the client must
identify those errors in order to reconstruct the error to use the same value
(when using package variables) or type (when using specific types) for allowing
the caller to be able to identify the error.

The 2 mentioned issues can be solved by errors which are identified by codes and
without the need of using package variables nor specific error types, just using
a minimal and simple public API exposed by this package. Nonetheless this
package doesn't attempt to fit to all the use cases, it fits to several of the
uses cases which were found in my experience, but they are not all. Hence,
before using it, assess if it can bring the mentioned benefits to your
implementation (for example a minimal library may have enough just returning
standard errors or using a couple of package variables error values variables or
specific types).

## Errors information

Errors are for users, but they must be useful for operations, too. In order to
achieve both, the error type of this package is endowed with several information.

1. A code and a static message. Both information is useful for users and
operations, because each code should be quite specific and self descriptive for
providing a synthesized information about the error which has happened. Having
a specific error code is also good to have each error properly documented, so it
can provide more detailed information about the error when its synthesized
information isn't enough.
2. An unique ID. Each error instance has an unique ID. For users, it's useful
because they could report it to the support team, a part of the code. Such ID
could be helpful to provide a customized feedback/response when needed and for
the operations team, could use the ID to correlate errors, when they are
registered/tracked in different operational systems or, for any reason, in the
same one several times.
3. Metadata. Despite that the code, and its associated message, should be
precise, the operations team needs more information about what happened when the
error has happened in some circumstances which aren't clear, for example the
input parameter values, variable values, etc. Developers should have a way to
provide such important context information when creating the errors and that's
what, in this package, is called metadata.
4. The call stack. Call stacks are ugly, but they provide the trace where the
error was originated and such information is very useful for the operations team
and maintainers, when the error has happened in unclear circumstances.
5. Original error. The most of the times, third party packages are used and,
obviously, those package don't probably use errors created by this package,
hence they don't have all the information, or a lest in the same way. Your
implementation has committed to return error with codes, but the original error
must be preserved because it may have additional information which may be useful
for the operations team and maintainers.

In summary, from the point of view of users, the error should be precise and
concise, without having any useless information and avoiding to leak important
information about the system; on the other hand, the operations team and
maintainers need much more information about the errors in order of being able
to understand the cause of the error.

## Errors are standard errors

This package doesn't intentionally export the error type, because:

1. Functions' signatures, which return errors, should always return a standard
error.
2. Type assertion for finding out errors information is less than ideal in
comparison on having an API.

Hence, this package export some functions to get information about the error,
however it only intentionally allows to get a part of it, because the
information destined for operations is only thought to be exposed through
systems for such purpose, for example logging.

The import path of this package is `go.fraixed.es/errors`.

## License

Expand Down
60 changes: 60 additions & 0 deletions callstack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package errors

import (
"fmt"
"runtime"
)

type callStack []uintptr

// newCallStack creates a callStack of calls skipping the call to
// runtime.Callers, newCallStack and the caller of newCallStack.
// The newCallStack is skipped because it's meant to be used by the errors
// consturctors and they shouldn't appear in the error value call stack.
func newCallStack() callStack {
var (
depth = 20
pcs = make([]uintptr, depth)
l = runtime.Callers(3, pcs)
)

for l == depth {
depth += 10
pcs = make([]uintptr, depth)
l = runtime.Callers(3, pcs)
}

pcs = pcs[:l]

return callStack(pcs)
}

// Format satisfies the fmt.Formatter interface.
// It only prints the value if the verb 'v' and flag '+' are used, printing the
// complete function identifier (package path + name), the file and the line.
// When the additional '-' is used, then only the function identifier is
// printed.
func (cs callStack) Format(state fmt.State, verb rune) {
if verb != 'v' {
return
}

if len(cs) > 0 {
var frs = runtime.CallersFrames(cs)
for {
var f, more = frs.Next()

if state.Flag('-') {
_, _ = fmt.Fprintf(state, "\t%s", f.Function)
} else {
_, _ = fmt.Fprintf(state, "\t%s\n\t\t%s:%d", f.Function, f.File, f.Line)
}

if !more {
break
}

fmt.Fprint(state, "\n")
}
}
}
54 changes: 54 additions & 0 deletions callstack_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package errors

import (
"fmt"
"math/rand"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCallStack_Format(t *testing.T) {
var (
ptName = t.Name()
cstk callStack
skippedFn = func() { cstk = newCallStack() }
f1 = func() { skippedFn() }
)
f1()

t.Run("'v' verb", func(t *testing.T) {
var s = fmt.Sprintf("%v", cstk)
var sls = strings.Split(s, "\n")

assert.Contains(t, sls[0], fmt.Sprintf("errors.%s.func2", ptName))
assert.Contains(t, sls[1], "errors/callstack_test.go:17")
})

t.Run("'v' verb and '-' flags", func(t *testing.T) {
var s = fmt.Sprintf("%-v", cstk)
var sls = strings.Split(s, "\n")

assert.Contains(t, sls[0], fmt.Sprintf("errors.%s.func2", ptName))
})

t.Run("any other verb", func(t *testing.T) {
var verbs = [...]string{
"t", "b", "c", "d", "o", "q", "x", "X", "U", "e", "E", "f", "F", "g", "G", "q", "p", "s",
}

var f = fmt.Sprintf("%%%s", verbs[rand.Intn(len(verbs))])
var s = fmt.Sprintf(f, cstk)
assert.Empty(t, s)
})

t.Run("empty stack", func(t *testing.T) {
var (
ecstk = callStack(nil)
s = fmt.Sprintf("%v", ecstk)
)

assert.Empty(t, s)
})
}
7 changes: 7 additions & 0 deletions code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package errors

// Code is the interface that any error code must satisfies.
type Code interface {
String() string
Message() string
}
38 changes: 38 additions & 0 deletions constructors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package errors

import "github.com/gofrs/uuid"

// New creates a new error with c and mds.
func New(c Code, mds ...MD) error {
var id, _ = uuid.NewV4()

return derror{
c: c,
id: id,
mds: mds,
cs: newCallStack(),
}
}

// Wrap creates a new error with c and mds, wrapping err.
func Wrap(err error, c Code, mds ...MD) error {
var (
id, _ = uuid.NewV4()
derr = derror{
c: c,
id: id,
mds: mds,
cs: newCallStack(),
}
)

if de, ok := err.(derror); ok {
de.cs = nil

derr.werr = de
} else {
derr.werr = err
}

return derr
}

0 comments on commit 99bdc50

Please sign in to comment.