template/pkg/result/expect_panic.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

106 lines
3.2 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
// 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 (
"errors"
"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 <- Fail[T](se)
return
}
panic(v) // genuine runtime panic — crash the program
}
if finished {
ch <- Ok(val)
return
}
ch <- Fail[T](errors.New("goroutine exited unexpectedly"))
}()
val = fn()
finished = true
}()
return ch
}