120 lines
3.6 KiB
Go
120 lines
3.6 KiB
Go
//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 ": <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 <- 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
|
||
}
|