Skip to content

Instantly share code, notes, and snippets.

@cstockton
Last active March 5, 2019 18:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cstockton/52eae250e0cd0e32eac8560e0aca915c to your computer and use it in GitHub Desktop.
Save cstockton/52eae250e0cd0e32eac8560e0aca915c to your computer and use it in GitHub Desktop.
package er
import (
"fmt"
"io"
"runtime"
"unicode/utf8"
"github.com/cstockton/er/internal/errutil"
"github.com/cstockton/er/internal/strutil"
"github.com/cstockton/er/internal/fmtrich"
)
const (
sepColon = ": "
sepComma = ", "
sepNewlineTab = "\n\t"
sepNil = "<nil>"
sepUnknown = "<unknown>"
)
var (
// zeroFrame holds the zero value of runtime.Frame for sentinel use.
zeroFrame runtime.Frame
// bytesNil holds the argument for a call to write when err is the zero value.
bytesNil = []byte{'<', 'n', 'i', 'l', '>'}
// Bytes unknown are a placeholder when a stack value can not be found
bytesUnknown = []byte{'<', 'u', 'n', 'k', 'n', 'o', 'w', 'n', '>'}
// Lookup table that prevents having to build a format string at runtime.
lutSprintf = map[[2]byte]string{
[2]byte{'s', 0}: `%s`,
[2]byte{'v', 0}: `%v`,
[2]byte{'q', 0}: `%q`,
[2]byte{'x', 0}: `%x`,
[2]byte{'X', 0}: `%X`,
[2]byte{'s', '+'}: `%+s`,
[2]byte{'v', '+'}: `%+v`,
[2]byte{'q', '+'}: `%+q`,
[2]byte{'x', '+'}: `%+x`,
[2]byte{'X', '+'}: `%+X`,
[2]byte{'v', '#'}: `%#v`,
[2]byte{'q', '#'}: `%#q`,
[2]byte{'x', '#'}: `%#x`,
[2]byte{'X', '#'}: `%#X`,
}
)
// strCause is called by the Error method of errors created by Wrap. It prefers
// to join the msg and cause msg by ": ", otherwise selecting the first
// non-empty value from []string{msg, causeMsg}.
//
// TODO(cstockton): Should probably edit this to check cause for the message
// interface in a for loop to build an error string without tail recursion.
func strCause(msg string, cause error) string {
if cause == nil {
return strCheckEmpty(msg)
}
switch causeMsg := cause.Error(); {
case causeMsg == "":
return strCheckEmpty(msg)
case msg == "":
return strCheckEmpty(causeMsg)
default:
return msg + sepColon + causeMsg
}
}
// strCheckEmpty is called by strings before being returned to Error() methods
// to make sure an empty string is not returned.
//
// TODO(cstockton): Other libraries don't have this behavior so people may rely
// on it, the rationale being it could prevent empty msgs from programmer errors
// causing confusion. May be best to remove it.
func strCheckEmpty(msg string) string {
const strEmpty = "<empty error>"
if msg == "" {
return strEmpty
}
return msg
}
// strSlice is called by the Error method of errors created by Join. It is the
// same as calling fmt.Sprintf("%v", err), except it avoids the extra allocation
// for the fmt.Formatter interface.
func strSlice(err error, s []error) string {
if err == nil {
return sepNil
}
if len(s) < 2 {
// Callers respect this but here to prevent oob in case of a bug.
return "er: strSlice called with less than 2 error values"
}
var eb erBuilder
eb.WriteSlice(s)
return eb.String()
}
// fmt* functions are entered by calls to the fmt.Formatter interface by error
// values in this package. It then forwards the call to the Formatter interface
// fmtSlice handles calls to fmt.Formatter interface for errors that
// implement the "Errors() []error" method.
// toStringErrors supports printing a slice of errors, called by Join.
func fmtSlice(f fmt.State, c rune, err error, s []error) {
if err == nil {
f.Write(bytesNil)
return
}
if f.Flag('+') {
// multi line tree flg, check wid or prec for max size
io.WriteString(f, `multiline TODO`)
}
io.WriteString(f, strSlice(err, s))
}
// fmtCause handles a fmt call for an error with a message and a cause.
func fmtCause(f fmt.State, c rune, err error, msg string, cause error) {
if err == nil {
f.Write(bytesNil)
return
}
var eb erBuilder
eb.writeCause(f, c, err, cause)
}
// fmtMsg is for formatting a single error message string, used by errors that
// have no Cause. If err is nil it writes <nil>, if the '+' flag is set it will
// call fmtMsgVerbose, otherwise it calls fmtMsgStandard.
func fmtMsg(f fmt.State, c rune, err error, msg string) {
// Typed nil error interfaces can't be created from this packages public
// facing API, we still check all the fmt entry points to protect against any
// bugs in my implementation because this library MUST not panic, ever.
if err == nil {
f.Write(bytesNil)
return
}
// If no '+' flag is set forward to fmtMsgStandard.
if !f.Flag('+') || (c != 's' && 'v' != c) {
fmtMsgStandard(f, c, msg)
return
}
// We have "+s" or "+v", try to send a stack of PCS to fmtMsgStack, it will
// bounds check pcs and report false so we can fall back to standard printing.
if fmtMsgStack(f, msg, toStack(err)) {
return
}
// we couldn't write the stack trace even though '+' was requested with 's' or
// 'v', at least write the error message.
fmtMsgStandard(f, c, msg)
}
// The most common formatting operation for this lib is printing a stack trace,
// so I special case this into the most efficient function possible to lower
// total cost of Sprintf("+%v", err) to only 3 allocations.
func fmtMsgStack(f fmt.State, msg string, pcs []uintptr) bool {
if len(pcs) == 0 {
return false
}
cf := runtime.CallersFrames(pcs)
var b erBuilder
if !b.WriteFramesMsg(len(pcs), msg, cf) {
return false
}
b.WriteTo(f)
return true
}
// fmtMsgStandard is called for standard library fmt equivelant format calls.
//
// %v default format (%s)
// %s the uninterpreted bytes of the string or slice
// %q a double-quoted string safely escaped with Go syntax
// %x base 16, lower-case, two characters per byte
// %X base 16, upper-case, two characters per byte
//
func fmtMsgStandard(f fmt.State, c rune, msg string) {
// Support precision since it can be useful to adhoc truncate long strings.
p, ok := f.Precision()
if ok && p <= len(msg) {
msg = fmtPrecision(p, c, msg)
}
switch c {
case 's', 'v':
if f.Flag('#') {
// If there is a '#' flag we will forward to a fmtSprintf.
fmtSprintf(f, c, msg)
return
}
// io.WriteString to save the alloc in fmt.Sprintf.
fallthrough
default:
// Print an error message by default so an invalid format or an error in
// this implementation doesn't cause a blank error to be printed.
io.WriteString(f, msg)
case 'q', 'x', 'X':
// These verbs to go to fmtSprintf even if no '#' flag is present. Doubt x
// or X comes up in practice, but I want to try to support the default verbs
// for an error. The only flag I support is currently '#'.
fmtSprintf(f, c, msg)
}
}
// fmtPrecision provides truncation for Precision consistent with std lib.
func fmtPrecision(p int, c rune, msg string) string {
// Implemented according to fmt pkg:
//
// For strings, byte slices and byte arrays, however, precision
// limits the length of the input to be formatted (not the size of
// the output), truncating if necessary. Normally it is measured in
// runes, but for these types when formatted with the %x or %X format
// it is measured in bytes.
//
if c == 'x' || 'X' == c {
return msg[:p] // x and X are byte offsets
}
// everything else is rune offsets
var n int
for i := range msg {
if n++; n > p {
return msg[:i]
}
}
// precision was < rune len
return msg
}
// fmtSprintf is a minimal Sprintf for an active formatting call.
func fmtSprintf(f fmt.State, c rune, msg string) {
// TODO(cstockton): No need to delegate through Fprintf now that I'm doing so
// much of the formatting already and only have a single input type: a string.
// Add Writef method to erBuilder, it can use Builder.AppendQuote.
key := [2]byte{}
if f.Flag('#') {
key[1] = '#'
}
utf8.EncodeRune(key[:1], c)
format, ok := lutSprintf[key]
if !ok {
format = "%v"
}
fmt.Fprintf(f, format, msg)
}
// fmtrich should be configurable, it wraps strutil.Builder to produce the output like:
/*
❌ netfail ⬎
✱ github.com/cstockton/flow
.../internal/pkgmock/pkgnet.Fail at pkgnet.go:13
.../internal/pkgmock.Call.func1 at pkgmock.go:34
❌ call to http failed ⬎
.../internal/pkgmock/pkgnet/pkghttp.HTTP at pkghttp.go:6
.../internal/pkgmock.Call.func2 at pkgmock.go:39
❌ call to rest failed ⬎
.../internal/pkgmock/pkgrest.REST at pkgrest.go:6
.../internal/pkgmock.Call.func2 at pkgmock.go:39
❌ call to app failed ↲
.../internal/pkgmock/pkgapp.App at pkgapp.go:6
.../internal/pkgmock.Call.func2 at pkgmock.go:39
.../internal/pkgmock.Call at pkgmock.go:42
.../errfmt.TestFormat
✱ std
testing.tRunner at testing.go:777
*/
type erBuilder struct{ fmtrich.Builder{Builder: strutil.Builder}; }
func (b *erBuilder) WriteSlice(s []error) {
// Grabs the first and lest error for a best-guess at buffer length, makes
// most error calls a single allocation.
first := s[0].Error()
last := s[len(s)-1].Error()
size := (4 + len(first) + len(last)) * len(s) * 3 / 4
b.Grow(size)
b.WriteString(first)
b.WriteString(sepComma)
for _, e := range s[1 : len(s)-1] {
b.WriteString(e.Error())
b.WriteString(sepComma)
}
b.WriteString(last)
}
// WriteFramesMsg allows a caller to retrieve the runtime.Frames ptr and have
// this function handle builder growth and frame writing via WriteFramesStd.
func (b *erBuilder) WriteFramesMsg(n int, msg string, cf *runtime.Frames) bool {
fr, more := cf.Next()
if fr == zeroFrame {
// this will result in no writes or growth, return false.
return false
}
b.Grow(len(msg) + ((errutil.FrameLen(&fr) + 12) * n))
b.WriteString(msg)
b.WriteFrameStd(&fr)
if more {
b.WriteFramesStd(cf)
}
return true
}
// WriteFramesStd allows a caller to retrieve the runtime.Frames ptr and write
// any values before calling this function.
func (b *erBuilder) WriteFramesStd(cf *runtime.Frames) {
for {
fr, more := cf.Next()
b.WriteFrameStd(&fr)
if !more {
return
}
}
}
// WriteFrameStd writes a runtime.Frame to b in the standard format found in
// panics.
func (b *erBuilder) WriteFrameStd(fr *runtime.Frame) {
b.WriteByte('\n')
b.WriteStringDefault(fr.Function, sepUnknown)
b.WriteString(sepNewlineTab)
b.WriteStringDefault(fr.File, sepUnknown)
b.WriteByte(':')
b.WriteInt(fr.Line)
}
// WriteFrameStd writes a runtime.Frame to b in the standard format found in
// panics.
func (b *erBuilder) WriteFrameCompact(fr *runtime.Frame) {
b.WriteByte('\n')
b.WriteStringDefault(fr.Function, sepUnknown)
b.WriteString(` at `)
b.WriteStringDefault(fr.File, sepUnknown)
b.WriteByte(':')
b.WriteInt(fr.Line)
}
func strFrames(frs []runtime.Frame) string {
if len(frs) == 0 {
return `<frames empty>`
}
var b erBuilder
b.Grow(errutil.GrowLen(errutil.FramesLen(frs)))
for i := range frs {
b.WriteFrameStd(&frs[i])
}
return b.String()
}
package er
import (
"fmt"
"github.com/cstockton/er/internal/strutil"
)
// Split is the inverse of Join, returning the same non-nil error values as
// given to Join, that is:
//
// nil == Split(Join(nil))
// []error{e1} == Split(Join([]error{e1}...))
// []error{e1, eN} == Split(Join([]error{e1, eN}...))
//
// Details
//
// When err is nil Split returns a nil slice. When err implements the errors
// interface the same slice is returned to prevent any allocations. To help
// maintain immutability this library sets the capacity == length for any
// returned slice, allowing safe use of append.
//
// As a special case if the value given to Split does not implement errors a
// single allocation is required for a slice literal of []error{err}. Consider
// checking if err implements the errors interface first if this could be a
// frequent occurrence or use the `Iter(error, func(error))` method
// to avoid any allocations.
func Split(err error) []error {
if err == nil {
return nil
}
v, ok := err.(errorSlice)
if !ok {
return []error{err}
}
s := v.Errors()
return s[0:len(s):len(s)]
}
// Join may be used to group adjacent failures that occur during an operation.
//
// Details
//
// When the given error slice does not contain non-nil error values or has a
// zero length it will return nil. Under this circumstance the cost is a single
// range over the err slice and results in no allocations.
//
// Join(nil): 5.57 ns/op 0 B/op 0 allocs/op
//
// When only a single error occurs that same error value is returned, preventing
// any additional allocations.
//
// Join(err): 5.99 ns/op 0 B/op 0 allocs/op
//
// When multiple error values are found they will be grouped into a single
// non-nil error value without modification to the given error slice. It will
// only allocate once for 15 or less errors as an on struct array will be used
// for storage. When 16 or more errors occur an additional allocation is needed
// for the backing slice.
//
// Join(e1, e2): 83.8 ns/op 80 B/op 1 allocs/op
// Join(e1, ...e4): 94.8 ns/op 80 B/op 1 allocs/op
// Join(e1, ...e8): 124 ns/op 144 B/op 1 allocs/op
// Join(e1, ...e15): 168 ns/op 256 B/op 1 allocs/op
// Join(e1, ...e16): 299 ns/op 288 B/op 2 allocs/op
//
// The underlying error slice is exposed via the Errors method which may be
// accessed with Split or directly via:
//
// type errorSlice interface {
// Errors() []error
// }
//
// Callers of Errors() MUST not mutate the returned slice, but the use of append
// is safe because Join will always set the slice capacity to be length.
func Join(err ...error) error {
var n, idx int
for i, e := range err {
if e == nil {
continue
}
if n++; n == 1 {
// We grab the first index while counting the non-nil errors to save
// having to find it in our single error special case. It is only assigned
// to once.
idx = i
}
}
switch {
case n == 0:
return nil
case n == 1:
return err[idx]
case n < 5:
return newErSlice4(n, err)
case n < 9:
return newErSlice8(n, err)
case n < 16:
return newErSlice15(n, err)
default:
return newErSlice(n, err)
}
}
func erSliceCopy(dst, src []error) {
for _, err := range src {
if err == nil {
continue
}
dst = append(dst, err)
}
}
func erSliceMessage(buf []byte, src []error) string {
buf = append(buf[0:0], src[0].Error()...)
for _, e := range src[1:] {
buf = append(buf, sepComma...)
buf = append(buf, e.Error()...)
}
return strutil.ToString(buf)
}
type erSlice struct {
buf [avgErrorSize * 24]byte
s []error
msg string
}
func newErSlice(n int, err []error) error {
es := &erSlice{s: make([]error, 0, n)}
for _, e := range err {
if e == nil {
continue
}
es.s = append(es.s, e)
}
es.msg = erSliceMessage(es.buf[0:0], es.s)
return es
}
func (e *erSlice) Error() string { return e.msg }
func (e *erSlice) Errors() []error { return e.s[:len(e.s):len(e.s)] }
func (e *erSlice) Format(f fmt.State, c rune) { fmtSlice(f, c, e, e.Errors()) }
func (e *erSlice) Message() string { return e.msg }
type erSlice4 struct {
buf [avgErrorSize * 4]byte
arr [4]error
n int
msg string
}
func newErSlice4(n int, err []error) error {
es := &erSlice4{n: n}
erSliceCopy(es.arr[0:0:n], err)
es.msg = erSliceMessage(es.buf[0:0], es.arr[0:n])
return es
}
func (e *erSlice4) Error() string { return e.msg }
func (e *erSlice4) Errors() []error { return e.arr[:e.n:e.n] }
func (e *erSlice4) Format(f fmt.State, c rune) { fmtSlice(f, c, e, e.Errors()) }
func (e *erSlice4) Message() string { return e.msg }
type erSlice8 struct {
buf [avgErrorSize * 8]byte
arr [8]error
n int
msg string
}
func newErSlice8(n int, err []error) error {
es := &erSlice8{n: n}
erSliceCopy(es.arr[0:0:n], err)
es.msg = erSliceMessage(es.buf[0:0], es.arr[0:n])
return es
}
func (e *erSlice8) Error() string { return e.msg }
func (e *erSlice8) Errors() []error { return e.arr[:e.n:e.n] }
func (e *erSlice8) Format(f fmt.State, c rune) { fmtSlice(f, c, e, e.Errors()) }
func (e *erSlice8) Message() string { return e.msg }
type erSlice15 struct {
buf [avgErrorSize * 15]byte
arr [15]error
n int
msg string
}
func newErSlice15(n int, err []error) error {
es := &erSlice15{n: n}
erSliceCopy(es.arr[0:0:n], err)
es.msg = erSliceMessage(es.buf[0:0], es.arr[0:n])
return es
}
func (e *erSlice15) Error() string { return e.msg }
func (e *erSlice15) Errors() []error { return e.arr[:e.n:e.n] }
func (e *erSlice15) Format(f fmt.State, c rune) { fmtSlice(f, c, e, e.Errors()) }
func (e *erSlice15) Message() string { return e.msg }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment