template/pkg/result/expect_goexit.go
djmil e204e43e6e use panics as a default error reporting mechanism
- runtime.Goexit() has too much performance overhead, and should be used only under special conditions
- introduce build tags
2026-04-23 21:05:18 +00:00

121 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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 (
"errors"
"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 ": <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
}
// 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
}