Last active
March 5, 2019 18:32
-
-
Save cstockton/52eae250e0cd0e32eac8560e0aca915c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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