//go:build result_goexit // Optional runtime.Goexit implementation, enabled with -tags result_goexit. // // Expect and Expectf exit the goroutine via runtime.Goexit instead of panic. // Goexit runs all deferred functions but is invisible to recover() — no // recover() call anywhere in the call chain can accidentally swallow a result // failure, making this build safe to use alongside frameworks or user code // that uses bare recover() inside Go/Run closures. // // Cost: error path ≈ 5.5 µs / ~536 B per failure (goroutineID parse + // sync.Map store/load + Goexit unwind) — about 4× the default panic build. // Happy-path cost is identical to the default. package result import ( "fmt" "runtime" "sync" ) // 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 // 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 } // 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 ": ". // // 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 } // 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 <- Err[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 <- Err[T](stored.(error)) } else { ch <- Errf[T]("goroutine exited unexpectedly") } }() val = fn() finished = true }() return ch }