template/pkg/result/result.go
2026-05-04 22:45:33 +00:00

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 }