komuw / ong

ong, is a Go http toolkit.
MIT License
16 stars 4 forks source link

ong/errors: add Join #443

Closed komuw closed 3 weeks ago

komuw commented 1 month ago

wrapper around stdlib Join

komuw commented 3 weeks ago

Even fmt.Errorf

komuw commented 3 weeks ago

func Join(errs ...error) error {
    // for _, e := range errs {
    //  if _, ok := e.(*stackError); ok {
    //      return e
    //  }
    // }

    // // worst case.
    // return wrap(errors.Join(errs...), 3)

    n := 0
    for _, err := range errs {
        if err != nil {
            n++
        }
    }
    if n == 0 {
        return nil
    }

    e := &join{
        errs: make([]error, 0, n),
    }
    for _, err := range errs {
        if err != nil {
            e.errs = append(e.errs, wrap(err, 3))
        }
    }
    return e
}

type join struct {
    errs []error
}

func (e *join) Error() string {
    var b []byte
    for i, err := range e.errs {
        if i > 0 {
            b = append(b, '\n')
        }
        b = append(b, err.Error()...)
    }
    return string(b)
}

func (e *join) Unwrap() error {
    if len(e.errs) > 0 {
        return e.errs[0]
    }
    return nil
}
komuw commented 3 weeks ago
// Package errors implements functions to manipulate errors.
package errors

import (
    "errors"
    "fmt"
    "io"
    "runtime"
    "strings"
)

// Some of the code here is inspired(or taken from) by:
//   (a) https://github.com/golang/pkgsite whose license(BSD 3-Clause "New") can be found here: https://github.com/golang/pkgsite/blob/24f94ffc546bde6aae0552efa6a940041d9d28e1/LICENSE
//   (b) https://www.komu.engineer/blogs/08/golang-stacktrace

// stackError is an implementation of error that adds stack trace support and error wrapping.
type stackError struct {
    stack []uintptr
    err   error
}

func (e *stackError) Error() string {
    return e.err.Error() // ignore the stack
}

// Unwrap unpacks wrapped errors.
func (e *stackError) Unwrap() error {
    return e.err
}

// New returns an error with the supplied message.
// It also records the stack trace at the point it was called.
//
// Error values implement [fmt.Formatter] and can be formatted by the fmt package. The following verbs are supported:
//
//  %s   print the error.
//  %v   see %s
//  %+v  print the error and stacktrace.
func New(text string) error {
    return wrap(errors.New(text), 3)
}

// Wrap returns err, capturing a stack trace.
// It is a no-op if err had already been wrapped by this library.
func Wrap(err error) error {
    return wrap(err, 3)
}

// Dwrap adds stack traces to the error.
// It does nothing when *errp == nil.
func Dwrap(errp *error) {
    if *errp != nil {
        *errp = wrap(*errp, 3)
    }
}

func wrap(err error, skip int) *stackError {
    c, ok := err.(*stackError)
    if ok {
        return c
    }

    // limit stack size to 64 call depth.
    // `pkgsite/derrors` limits it to 16K(16 * 1024)
    // https://github.com/golang/pkgsite/blob/035bfc02f3faa0221e0edf90b0a21d3619c95fdd/internal/derrors/derrors.go#L261-L264
    stack := [64]uintptr{}
    // skip 0 identifies the frame for `runtime.Callers` itself and
    // skip 1 identifies the caller of `runtime.Callers`(ie of `wrap`).
    n := runtime.Callers(skip, stack[:])

    return &stackError{
        err:   err,
        stack: stack[:n],
    }
}

func (e *stackError) getStackTrace() string {
    var trace strings.Builder
    frames := runtime.CallersFrames(e.stack[:])
    for {
        frame, more := frames.Next()
        if !strings.Contains(frame.File, "runtime/") { // we cant use something like "go/src/runtime/" since it will break for programs built using `go build -trimpath`
            trace.WriteString(fmt.Sprintf("\n%s:%d", frame.File, frame.Line))
        }
        if !more {
            break
        }
    }
    return trace.String()
}

// Format implements the fmt.Formatter interface
func (e *stackError) Format(f fmt.State, verb rune) {
    switch verb {
    case 'v':
        if f.Flag('+') {
            _, _ = io.WriteString(f, e.Error())
            _, _ = io.WriteString(f, e.getStackTrace())
            return
        }
        fallthrough
    case 's':
        _, _ = io.WriteString(f, e.Error())
    case 'q':
        _, _ = fmt.Fprintf(f, "%q", e.Error())
    }
}

// StackTrace returns the stack trace contained in err, if any, else an empty string.
func StackTrace(err error) string {
    sterr, ok := err.(*stackError)
    if !ok {
        return ""
    }
    return sterr.getStackTrace()
}

func Join(errs ...error) error {
    n := 0
    for _, err := range errs {
        if err != nil {
            n++
        }
    }
    if n == 0 {
        return nil
    }

    e := &join{errs: make([]error, 0, n)}
    for _, err := range errs {
        if err != nil {
            ef := wrap(err, 3)
            e.errs = append(e.errs, ef)
            if e.stackError == nil {
                e.stackError = ef
            }
        }
    }

    return e
}

type join struct {
    *stackError
    errs []error
}

func (e *join) Error() string {
    var b []byte
    for i, err := range e.errs {
        if i > 0 {
            b = append(b, '\n')
        }
        b = append(b, err.Error()...)
    }
    return string(b)
}

func (e *join) Unwrap() error {
    if len(e.errs) > 0 {
        return e.errs[0]
    }
    return nil
}
komuw commented 3 weeks ago
// Package errors implements functions to manipulate errors.
package errors

import (
    "errors"
    "fmt"
    "io"
    "runtime"
    "strings"
)

// Some of the code here is inspired(or taken from) by:
//   (a) https://github.com/golang/pkgsite whose license(BSD 3-Clause "New") can be found here: https://github.com/golang/pkgsite/blob/24f94ffc546bde6aae0552efa6a940041d9d28e1/LICENSE
//   (b) https://www.komu.engineer/blogs/08/golang-stacktrace

// stackError is an implementation of error that adds stack trace support and error wrapping.
type stackError struct {
    stack []uintptr
    err   error
}

func (e *stackError) Error() string {
    return e.err.Error() // ignore the stack
}

// Unwrap unpacks wrapped errors.
func (e *stackError) Unwrap() error {
    return e.err
}

// New returns an error with the supplied message.
// It also records the stack trace at the point it was called.
//
// Error values implement [fmt.Formatter] and can be formatted by the fmt package. The following verbs are supported:
//
//  %s   print the error.
//  %v   see %s
//  %+v  print the error and stacktrace.
func New(text string) error {
    return wrap(errors.New(text), 3)
}

// Wrap returns err, capturing a stack trace.
// It is a no-op if err had already been wrapped by this library.
func Wrap(err error) error {
    return wrap(err, 3)
}

// Dwrap adds stack traces to the error.
// It does nothing when *errp == nil.
func Dwrap(errp *error) {
    if *errp != nil {
        *errp = wrap(*errp, 3)
    }
}

func wrap(err error, skip int) *stackError {
    c, ok := err.(*stackError)
    if ok {
        return c
    }

    // limit stack size to 64 call depth.
    // `pkgsite/derrors` limits it to 16K(16 * 1024)
    // https://github.com/golang/pkgsite/blob/035bfc02f3faa0221e0edf90b0a21d3619c95fdd/internal/derrors/derrors.go#L261-L264
    stack := [64]uintptr{}
    // skip 0 identifies the frame for `runtime.Callers` itself and
    // skip 1 identifies the caller of `runtime.Callers`(ie of `wrap`).
    n := runtime.Callers(skip, stack[:])

    return &stackError{
        err:   err,
        stack: stack[:n],
    }
}

func (e *stackError) getStackTrace() string {
    var trace strings.Builder
    frames := runtime.CallersFrames(e.stack[:])
    for {
        frame, more := frames.Next()
        if !strings.Contains(frame.File, "runtime/") { // we cant use something like "go/src/runtime/" since it will break for programs built using `go build -trimpath`
            trace.WriteString(fmt.Sprintf("\n%s:%d", frame.File, frame.Line))
        }
        if !more {
            break
        }
    }
    return trace.String()
}

// Format implements the fmt.Formatter interface
func (e *stackError) Format(f fmt.State, verb rune) {
    switch verb {
    case 'v':
        if f.Flag('+') {
            _, _ = io.WriteString(f, e.Error())
            _, _ = io.WriteString(f, e.getStackTrace())
            return
        }
        fallthrough
    case 's':
        _, _ = io.WriteString(f, e.Error())
    case 'q':
        _, _ = fmt.Fprintf(f, "%q", e.Error())
    }
}

// StackTrace returns the stack trace contained in err, if any, else an empty string.
func StackTrace(err error) string {
    if sterr, ok := err.(*stackError); ok {
        return sterr.getStackTrace()
    }
    if sterr, ok := err.(*join); ok {
        return sterr.getStackTrace()
    }

    return ""
}

func Join(errs ...error) error {
    n := 0
    for _, err := range errs {
        if err != nil {
            n++
        }
    }
    if n == 0 {
        return nil
    }

    e := &join{errs: make([]error, 0, n)}
    for _, err := range errs {
        if err != nil {
            ef := wrap(err, 3)
            e.errs = append(e.errs, ef)
            if e.stackError == nil {
                e.stackError = ef
            }
        }
    }

    return e
}

type join struct {
    *stackError
    errs []error
}

func (e *join) Error() string {
    var b []byte
    for i, err := range e.errs {
        if i > 0 {
            b = append(b, '\n')
        }
        b = append(b, err.Error()...)
    }
    return string(b)
}

func (e *join) Unwrap() error {
    if len(e.errs) > 0 {
        return e.errs[0]
    }
    return nil
}