template/pkg/result/result.go
djmil 974fed55d3 happy path perf optimization
move goroutineID() inside the defer, guarded by !finished. It's safe — Goexit runs defers in the same goroutine, so the ID is stable.
2026-04-23 20:02:34 +00:00

258 lines
7.4 KiB
Go

package result
import (
"errors"
"fmt"
"runtime"
"strings"
"sync"
)
// Expect holds either a value of type T or an error.
type Expect[T any] struct {
value T
err error
}
// gErrors stores errors set by Expect/Expectf before calling runtime.Goexit,
// keyed by goroutine ID. Entries are consumed by the enclosing Go or Run call.
var gErrors sync.Map
// 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
}
// goroutineID returns the current goroutine's numeric ID by parsing the first
// line of runtime.Stack output ("goroutine NNN [...]"). Called only on error
// paths so the runtime.Stack overhead is acceptable.
func goroutineID() uint64 {
var buf [64]byte
n := runtime.Stack(buf[:], false)
var id uint64
for _, b := range buf[10:n] { // skip "goroutine "
if b < '0' || b > '9' {
break
}
id = id*10 + uint64(b-'0')
}
return id
}
// 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
}
// Must returns the value or panics with the wrapped error and a stack trace.
// Prefer [Expect.Expect] — it exits via runtime.Goexit and is collected
// cleanly by [Go] or [Run]. Must is for call sites where a genuine unrecoverable
// condition warrants an immediate crash.
func (r Expect[T]) Must() T {
if r.err != nil {
panic(&stackError{err: r.err, stack: callers(3)})
}
return r.value
}
// Expect returns the value or exits the current goroutine via runtime.Goexit,
// storing the annotated error for collection by the enclosing [Go] or [Run]
// call. The stack trace is captured at this call site.
//
// data := Parse(raw).Expect("parse user input")
func (r Expect[T]) Expect(msg string) T {
if r.err != nil {
gErrors.Store(goroutineID(), &stackError{
err: fmt.Errorf("%s: %w", msg, r.err),
stack: callers(3),
})
runtime.Goexit()
}
return r.value
}
// Expectf is like [Expect.Expect] but accepts a fmt.Sprintf-style format string
// for the context message. The wrapped error is always appended as ": <err>".
//
// data := Parse(raw).Expectf("parse user input id=%d", id)
func (r Expect[T]) Expectf(format string, args ...any) T {
if r.err != nil {
gErrors.Store(goroutineID(), &stackError{
err: fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), r.err),
stack: callers(3),
})
runtime.Goexit()
}
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
}
// Async runs fn in a new goroutine and returns a channel that will receive
// exactly one Expect[T] when the goroutine finishes. The channel is buffered
// so the goroutine never blocks even if the caller is busy with other work.
//
// Use Async when you want to run several operations concurrently and collect
// their results later:
//
// aCh := result.Async(func() *A { return buildA().Expect("build A") })
// bCh := result.Async(func() *B { return buildB().Expect("build B") })
// a := (<-aCh).Expect("A")
// b := (<-bCh).Expect("B")
//
// Genuine runtime panics (nil-pointer dereferences, etc.) are not recovered —
// they still crash the program, as they should.
func Async[T any](fn func() T) <-chan Expect[T] {
ch := make(chan Expect[T], 1)
go func() {
var (
val T
finished bool
)
defer func() {
if v := recover(); v != nil {
if err, ok := v.(*stackError); ok {
// Must() panic — treat as a collected failure.
ch <- Fail[T](err)
return
}
panic(v) // genuine runtime panic — crash the program
}
if finished {
ch <- Ok(val)
return
}
// goroutineID is looked up here, on the error path only.
if stored, ok := gErrors.LoadAndDelete(goroutineID()); ok {
ch <- Fail[T](stored.(error))
} else {
ch <- Fail[T](errors.New("goroutine exited unexpectedly"))
}
}()
val = fn()
finished = true
}()
return ch
}
// 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 [Expect.Expect] and [Expect.Expectf] unwinding, use [Go] or [Run]
// instead — those exit via runtime.Goexit and are not recoverable by Catch.
// 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 }