105 lines
3.2 KiB
Go
105 lines
3.2 KiB
Go
//go:build !result_goexit
|
||
|
||
// Default error-exit implementation: Expect and Expectf signal failure via
|
||
// panic, which is caught by the enclosing result.Go / result.Run goroutine.
|
||
//
|
||
// Performance: error path ≈ 1.4 µs / ~352 B per failure (goroutine spawn +
|
||
// panic + recover). The runtime.Goexit build is about 4× slower (~5.5 µs).
|
||
//
|
||
// Trade-off: a deferred recover() inside a Go/Run closure that does not
|
||
// re-panic unrecognized values will silently swallow a result failure and let
|
||
// execution continue as if nothing happened. This follows standard Go practice
|
||
// for recover() — always check the type and re-panic anything unrecognized:
|
||
//
|
||
// defer func() {
|
||
// if r := recover(); r != nil {
|
||
// if _, ok := r.(MyExpectedType); !ok {
|
||
// panic(r) // not ours — let it propagate
|
||
// }
|
||
// // handle MyExpectedType ...
|
||
// }
|
||
// }()
|
||
//
|
||
// If your codebase uses bare recover() inside Go/Run closures, or integrates
|
||
// with frameworks that do, opt into the safer runtime.Goexit implementation:
|
||
//
|
||
// go test -tags result_goexit ./...
|
||
// go build -tags result_goexit ./...
|
||
|
||
package result
|
||
|
||
import (
|
||
"fmt"
|
||
)
|
||
|
||
// Expect returns the value or panics with the annotated error, which is
|
||
// collected by the enclosing [Go] or [Run] call. The stack trace is captured
|
||
// at this call site when [CaptureStack] is true.
|
||
//
|
||
// data := Parse(raw).Expect("parse user input")
|
||
func (r Expect[T]) Expect(msg string) T {
|
||
if r.err != nil {
|
||
panic(&stackError{
|
||
err: fmt.Errorf("%s: %w", msg, r.err),
|
||
stack: callers(3),
|
||
})
|
||
}
|
||
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 {
|
||
panic(&stackError{
|
||
err: fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), r.err),
|
||
stack: callers(3),
|
||
})
|
||
}
|
||
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 se, ok := v.(*stackError); ok {
|
||
// Expect/Must panic — treat as a collected failure.
|
||
ch <- Err[T](se)
|
||
return
|
||
}
|
||
panic(v) // genuine runtime panic — crash the program
|
||
}
|
||
if finished {
|
||
ch <- Ok(val)
|
||
return
|
||
}
|
||
ch <- Errf[T]("goroutine exited unexpectedly")
|
||
}()
|
||
val = fn()
|
||
finished = true
|
||
}()
|
||
return ch
|
||
}
|