164 lines
4.6 KiB
Go
164 lines
4.6 KiB
Go
package result
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
// Expect holds either a value of type T or an error.
|
|
type Expect[T any] struct {
|
|
value T
|
|
err error
|
|
}
|
|
|
|
// CaptureStack controls whether Expect, Expectf, and Must capture a stack
|
|
// trace at the failure site. Defaults to true. Set to false at program startup
|
|
// (before spawning goroutines) to cut ~1.5 KB of allocation and most of the
|
|
// error-path overhead; errors are still collected and propagated, just without
|
|
// a trace. StackTrace will return an empty string for errors captured while
|
|
// this is false.
|
|
var CaptureStack = true
|
|
|
|
// callers returns the program counters of the call stack starting at the
|
|
// caller's caller, skipping skip frames above runtime.Callers itself.
|
|
// Returns nil when CaptureStack is false.
|
|
func callers(skip int) []uintptr {
|
|
if !CaptureStack {
|
|
return nil
|
|
}
|
|
var pcs [32]uintptr
|
|
n := runtime.Callers(skip, pcs[:])
|
|
if n == 0 {
|
|
return nil
|
|
}
|
|
cp := make([]uintptr, n)
|
|
copy(cp, pcs[:n])
|
|
return cp
|
|
}
|
|
|
|
// Ok wraps a successful value in an Expect.
|
|
func Ok[T any](v T) Expect[T] {
|
|
return Expect[T]{value: v}
|
|
}
|
|
|
|
// Fail wraps an error in an Expect.
|
|
func Fail[T any](err error) Expect[T] {
|
|
return Expect[T]{err: err}
|
|
}
|
|
|
|
// Of is a convenience constructor that bridges standard Go (value, error)
|
|
// return signatures:
|
|
//
|
|
// result.Of(os.Open("file.txt")).Expect("open config")
|
|
func Of[T any](v T, err error) Expect[T] {
|
|
return Expect[T]{value: v, err: err}
|
|
}
|
|
|
|
// Err returns the wrapped error, or nil on success.
|
|
func (r Expect[T]) Err() error {
|
|
return r.err
|
|
}
|
|
|
|
// Value returns the underlying value, or the zero value of T if the Expect
|
|
// holds an error. Always check [Expect.Err] first.
|
|
func (r Expect[T]) Value() T {
|
|
return r.value
|
|
}
|
|
|
|
// Must returns the value or panics with the wrapped error and a stack trace.
|
|
// Use for genuine unrecoverable conditions where an immediate crash is correct.
|
|
// For normal error propagation inside [Go] or [Run], use [Expect.Expect].
|
|
func (r Expect[T]) Must() T {
|
|
if r.err != nil {
|
|
panic(&stackError{err: r.err, stack: callers(3)})
|
|
}
|
|
return r.value
|
|
}
|
|
|
|
// Unwrap returns the value and error in the standard Go (value, error) form.
|
|
// Useful at the boundary where you want to re-join normal error-return code.
|
|
func (r Expect[T]) Unwrap() (T, error) {
|
|
return r.value, r.err
|
|
}
|
|
|
|
// Go runs fn in a new goroutine and blocks until it completes, returning the
|
|
// result as Expect[T]. It is a convenience wrapper around [Async] for the
|
|
// common single-goroutine case.
|
|
//
|
|
// msg := result.Go(func() string {
|
|
// cfg := loadConfig().Expect("load config")
|
|
// return greet(cfg).Expect("greet")
|
|
// })
|
|
func Go[T any](fn func() T) Expect[T] {
|
|
return <-Async(fn)
|
|
}
|
|
|
|
// Run runs fn in a new goroutine and returns any error produced by
|
|
// [Expect.Expect] or [Expect.Expectf] calls within fn. Blocks until fn
|
|
// completes or the goroutine exits.
|
|
//
|
|
// func run() error {
|
|
// return result.Run(func() {
|
|
// port := parsePort(cfg.Port).Expect("load config port")
|
|
// _ = port // happy path continues …
|
|
// })
|
|
// }
|
|
func Run(fn func()) error {
|
|
return Go(func() struct{} {
|
|
fn()
|
|
return struct{}{}
|
|
}).Err()
|
|
}
|
|
|
|
// Catch recovers a panic produced by [Expect.Must] and stores it in *errp.
|
|
// For normal error propagation use [Go] or [Run] instead — they collect
|
|
// failures from [Expect.Expect] and [Expect.Expectf] regardless of build tag.
|
|
// Panics that are not errors (e.g. nil-pointer dereferences) are re-panicked.
|
|
func Catch(errp *error) {
|
|
v := recover()
|
|
if v == nil {
|
|
return
|
|
}
|
|
if err, ok := v.(error); ok {
|
|
*errp = err
|
|
return
|
|
}
|
|
panic(v) // not an error — let it propagate
|
|
}
|
|
|
|
// StackTrace returns the call stack captured when [Expect.Expect],
|
|
// [Expect.Expectf], or [Expect.Must] stored or panicked with an error.
|
|
// Frames are formatted as "function\n\tfile:line\n" starting at the call
|
|
// site of the failing Expect/Expectf/Must call.
|
|
// Returns an empty string if err was not produced by this package or if
|
|
// [CaptureStack] was false when the error was captured.
|
|
func StackTrace(err error) string {
|
|
var s *stackError
|
|
if !errors.As(err, &s) || len(s.stack) == 0 {
|
|
return ""
|
|
}
|
|
frames := runtime.CallersFrames(s.stack)
|
|
var b strings.Builder
|
|
for {
|
|
f, more := frames.Next()
|
|
if f.Function != "runtime.goexit" {
|
|
fmt.Fprintf(&b, "%s\n\t%s:%d\n", f.Function, f.File, f.Line)
|
|
}
|
|
if !more {
|
|
break
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// stackError wraps an error with program counters captured at the failure site.
|
|
type stackError struct {
|
|
err error
|
|
stack []uintptr // nil when CaptureStack is false
|
|
}
|
|
|
|
func (s *stackError) Error() string { return s.err.Error() }
|
|
func (s *stackError) Unwrap() error { return s.err }
|