result: Errw with caller info
- wrap existing errors with context + file:line, newline-separated for readable error chains - dual mode philosophy: panics + if err != nil - unify Expect for goexit and panic cases
This commit is contained in:
parent
7748ebfd89
commit
81f5a49cea
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@ -29,12 +29,13 @@
|
||||
"go.testExplorer.enable": true,
|
||||
"cSpell.words": [
|
||||
"djmil",
|
||||
"Errf",
|
||||
"Errw",
|
||||
"Expectf",
|
||||
"Failf",
|
||||
"gitea",
|
||||
"golangci",
|
||||
"nolint",
|
||||
"testutil",
|
||||
"Errf"
|
||||
"testutil"
|
||||
]
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import (
|
||||
|
||||
func main() {
|
||||
if err := result.Run(showGreeting); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "fatal: %v\n", err)
|
||||
fmt.Fprintf(os.Stderr, "[failed] %v\n", err)
|
||||
if stack := result.StackTrace(err); stack != "" {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", stack)
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func New(log *logger.Logger) *Service {
|
||||
// Greet returns a personalized greeting and logs the interaction.
|
||||
func (s *Service) Greet(name string) result.Expect[string] {
|
||||
if name == "" {
|
||||
return result.Errf[string]("Greet: name must not be empty")
|
||||
return result.Errf[string]("name must not be empty")
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Hello, %s!", name)
|
||||
|
||||
@ -32,7 +32,7 @@ type Logger struct {
|
||||
func New(level string) result.Expect[*Logger] {
|
||||
lvl := parseLevel(level)
|
||||
if lvl.Err() != nil {
|
||||
return result.Errf[*Logger]("parseLevel: %w", lvl.Err())
|
||||
return result.Errw[*Logger](lvl.Err(), "parse log level")
|
||||
}
|
||||
|
||||
handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl.Value()})
|
||||
@ -76,7 +76,7 @@ func (l *Logger) WithFields(fields map[string]any) *Logger {
|
||||
func parseLevel(level string) result.Expect[slog.Level] {
|
||||
var lvl slog.Level
|
||||
if err := lvl.UnmarshalText([]byte(level)); err != nil {
|
||||
return result.Errf[slog.Level]("unknown level %q (use debug|info|warn|error)", level)
|
||||
return result.Errw[slog.Level](err, "unknown level (use debug|info|warn|error)")
|
||||
}
|
||||
|
||||
return result.Ok(lvl)
|
||||
|
||||
@ -1,41 +1,61 @@
|
||||
// Package result provides a generic Expect[T] type for happy-path-oriented code.
|
||||
// Package result provides a generic Expect[T] type that supports two error
|
||||
// handling styles without forcing either one.
|
||||
//
|
||||
// # Purpose
|
||||
// # Two modes, one type
|
||||
//
|
||||
// result is a convenience tool for removing error-threading clutter from
|
||||
// application logic. Instead of propagating (value, error) pairs through every
|
||||
// frame, functions return Expect[T] and the caller unwraps at the boundary.
|
||||
// Expect[T] is a drop-in replacement for (T, error) that also enables
|
||||
// panic-based happy-path propagation when that suits the code better. Both
|
||||
// styles compose freely — the same Expect[T] value works in either.
|
||||
//
|
||||
// func parseHost(s string) result.Expect[string] {
|
||||
// if s == "" {
|
||||
// return result.Errf[string]("host must not be empty")
|
||||
// }
|
||||
// return result.Ok(s)
|
||||
// }
|
||||
//
|
||||
// Mode 1 — standard Go style (if err != nil):
|
||||
//
|
||||
// host, err := parseHost(s).Unwrap()
|
||||
// if err != nil {
|
||||
// return 0, err
|
||||
// }
|
||||
//
|
||||
// Or check and access separately, just as with (T, error):
|
||||
//
|
||||
// r := parseHost(s)
|
||||
// if r.Err() != nil {
|
||||
// return 0, r.Err()
|
||||
// }
|
||||
// use(r.Value())
|
||||
//
|
||||
// Mode 2 — happy-path style (panic-based propagation):
|
||||
//
|
||||
// port := parseHost(s).Expect("parse host") // panics on failure
|
||||
//
|
||||
// Failures are collected at the entry point by [Go] or [Run] and returned as a
|
||||
// normal Go error — no goroutine leaks, no silent swallowing.
|
||||
//
|
||||
// # Layering rule
|
||||
//
|
||||
// Reusable library code (packages under pkg/) must only *return* Expect[T] —
|
||||
// it must never call .Expect(), .Must(), or .Expectf() itself. Those methods
|
||||
// exit the current goroutine via runtime.Goexit and are only safe inside a
|
||||
// goroutine controlled by [Go] or [Run]. Calling them in a library takes
|
||||
// control away from the caller and makes the package non-composable.
|
||||
// exit the current goroutine and are only safe inside a goroutine controlled
|
||||
// by [Go] or [Run].
|
||||
//
|
||||
// The right split:
|
||||
//
|
||||
// - pkg/ functions: return Expect[T] or Expect[Nothing] — let the caller decide.
|
||||
// - pkg/ functions: return Expect[T] — let the caller decide how to handle it.
|
||||
// - Application code (cmd/, HTTP handlers, …): chain .Expect() calls freely,
|
||||
// protected by a defer result.Catch(&err) or a result.Run wrapper.
|
||||
//
|
||||
// # Intended pattern
|
||||
//
|
||||
// 1. Deep call stacks write for the happy path, using [Expect.Expect] or
|
||||
// [Expect.Expectf] to unwrap values — exiting the goroutine on failure
|
||||
// (panic by default; runtime.Goexit with -tags result_goexit) rather than
|
||||
// threading error returns through every frame.
|
||||
// 2. The entry point wraps the work with [Go] or [Run], which spawn the work
|
||||
// in a goroutine and collect any failure as a normal Go error.
|
||||
//
|
||||
// Stack traces are captured at the failure site and can be retrieved from the
|
||||
// collected error via [StackTrace].
|
||||
//
|
||||
// # Constructors
|
||||
//
|
||||
// Use [Ok] to wrap a success value, [Err] to wrap an error, and [Of] to
|
||||
// bridge existing (value, error) return signatures:
|
||||
// Use [Ok] to wrap a success value, [Err] / [Errf] / [Errw] to wrap errors,
|
||||
// and [Of] to bridge existing (value, error) return signatures:
|
||||
//
|
||||
// data := result.Of(os.ReadFile("cfg.json")).Expect("read config")
|
||||
//
|
||||
@ -43,8 +63,8 @@
|
||||
//
|
||||
// func run() error {
|
||||
// return result.Run(func() {
|
||||
// port := parsePort(cfg.Port).Expect("load config port")
|
||||
// _ = port // happy path continues …
|
||||
// host := parseHost(cfg.Host).Expect("load config host")
|
||||
// _ = host // happy path continues …
|
||||
// })
|
||||
// }
|
||||
//
|
||||
@ -55,8 +75,8 @@
|
||||
//
|
||||
// func load() (err error) {
|
||||
// defer result.Catch(&err)
|
||||
// port := parsePort(cfg.Port).Expect("load config port")
|
||||
// _ = port
|
||||
// host := parseHost(cfg.Host).Expect("load config host")
|
||||
// _ = host
|
||||
// return
|
||||
// }
|
||||
//
|
||||
|
||||
@ -19,7 +19,7 @@ func parseHost(s string) result.Expect[string] {
|
||||
func parsePort(s string) result.Expect[int] {
|
||||
port := result.Of(strconv.Atoi(s))
|
||||
if port.Err() != nil {
|
||||
return port
|
||||
return result.Errw[int](port.Err(), "result.Of")
|
||||
}
|
||||
if port.Value() < 1 || port.Value() > 65535 {
|
||||
return result.Errf[int]("%d out of range", port.Value())
|
||||
@ -27,15 +27,23 @@ func parsePort(s string) result.Expect[int] {
|
||||
return port
|
||||
}
|
||||
|
||||
// getURL is an example of business logic implementation with emphasis on happy-path.
|
||||
func getURL(host, port string) string {
|
||||
mHost := parseHost(host).Expect("parse host")
|
||||
mPort := parsePort(port).Expect("parse port")
|
||||
return fmt.Sprintf("http://%s:%d", mHost, mPort)
|
||||
}
|
||||
|
||||
// Example_errCheck shows checking the error without panicking — useful at the
|
||||
// outermost boundary where you want a normal error return.
|
||||
func Example_errCheck() {
|
||||
r := parsePort("not-a-number")
|
||||
if r.Err() != nil {
|
||||
fmt.Println("parsePort failed:", r.Err())
|
||||
fmt.Println("failed:", r.Err())
|
||||
}
|
||||
// Output:
|
||||
// parsePort failed: strconv.Atoi: parsing "not-a-number": invalid syntax
|
||||
// failed: example_test.go:22: result.Of
|
||||
// strconv.Atoi: parsing "not-a-number": invalid syntax
|
||||
}
|
||||
|
||||
// Example_happyPath shows the basic happy-path pattern: call Expect at each
|
||||
@ -59,16 +67,12 @@ func Example_catch() {
|
||||
|
||||
if err := loadHost(); err != nil {
|
||||
fmt.Println("caught:", err)
|
||||
// NOTE: you can have stack trace of an error as well
|
||||
// fmt.Println("stacktrace:", result.StackTrace(err))
|
||||
}
|
||||
// Output:
|
||||
// caught: read host: host must not be empty
|
||||
}
|
||||
|
||||
// getURL is an example of business logic implementation with emphasis on happy-path.
|
||||
func getURL(host, port string) string {
|
||||
mHost := parseHost(host).Expect("parse host")
|
||||
mPort := parsePort(port).Expect("parse port")
|
||||
return fmt.Sprintf("http://%s:%d", mHost, mPort)
|
||||
// caught: read host
|
||||
// example_test.go:13: host must not be empty
|
||||
}
|
||||
|
||||
// Example_run shows result.Run as a lightweight synchronous boundary — a concise
|
||||
@ -101,7 +105,8 @@ func Example_runtimeError() {
|
||||
fmt.Println("caught:", err)
|
||||
}
|
||||
// Output:
|
||||
// caught: arg 2 port value: 99999 out of range
|
||||
// caught: arg 2 port value
|
||||
// example_test.go:25: 99999 out of range
|
||||
}
|
||||
|
||||
// Example_go is like result.Run but returns a typed Expect[T] so the computed
|
||||
@ -109,9 +114,9 @@ func Example_runtimeError() {
|
||||
func Example_go() {
|
||||
url := result.Go(func() string {
|
||||
return getURL("localhost", "8080")
|
||||
})
|
||||
}).Must()
|
||||
|
||||
fmt.Println(url.Expect("get url"))
|
||||
fmt.Println(url)
|
||||
// Output:
|
||||
// http://localhost:8080
|
||||
}
|
||||
@ -127,7 +132,8 @@ func Example_goError() {
|
||||
fmt.Println("failed:", err)
|
||||
}
|
||||
// Output:
|
||||
// failed: parse port: 99999 out of range
|
||||
// failed: parse port
|
||||
// example_test.go:25: 99999 out of range
|
||||
}
|
||||
|
||||
// Example_unwrap shows re-joining the normal Go (value, error) world at a
|
||||
|
||||
@ -15,13 +15,12 @@
|
||||
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.
|
||||
// gErrors stores errors set by exitGoroutine before calling runtime.Goexit,
|
||||
// keyed by goroutine ID. Entries are consumed by the enclosing Async call.
|
||||
var gErrors sync.Map
|
||||
|
||||
// goroutineID returns the current goroutine's numeric ID by parsing the first
|
||||
@ -40,80 +39,18 @@ func goroutineID() uint64 {
|
||||
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),
|
||||
})
|
||||
// exitGoroutine stores se in gErrors and exits the goroutine via Goexit.
|
||||
// The stored error is retrieved by collectGoexitFailure in the enclosing Async.
|
||||
func exitGoroutine(se *stackError) {
|
||||
gErrors.Store(goroutineID(), se)
|
||||
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.
|
||||
// collectGoexitFailure retrieves any failure stored by exitGoroutine for the
|
||||
// current goroutine, consuming the entry.
|
||||
func collectGoexitFailure() error {
|
||||
if stored, ok := gErrors.LoadAndDelete(goroutineID()); ok {
|
||||
ch <- Err[T](stored.(error))
|
||||
} else {
|
||||
ch <- Errf[T]("goroutine exited unexpectedly")
|
||||
return stored.(error)
|
||||
}
|
||||
}()
|
||||
val = fn()
|
||||
finished = true
|
||||
}()
|
||||
return ch
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -28,77 +28,10 @@
|
||||
|
||||
package result
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
// exitGoroutine signals a result failure by panicking with se.
|
||||
// The panic is caught by the enclosing Async goroutine via recover().
|
||||
func exitGoroutine(se *stackError) { panic(se) }
|
||||
|
||||
// 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
|
||||
}
|
||||
// collectGoexitFailure is a no-op in the panic build — failures travel via
|
||||
// panic/recover, not via the gErrors map.
|
||||
func collectGoexitFailure() error { return nil }
|
||||
|
||||
@ -22,7 +22,7 @@ func TestMustCollected(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if err.Error() != "unrecoverable" {
|
||||
if !strings.Contains(err.Error(), "unrecoverable") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,11 +3,17 @@ package result
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Expect holds either a value of type T or an error.
|
||||
//
|
||||
// Fields are unexported to prevent external mutation (e.g. r.Err = nil silently
|
||||
// turning a failure into an apparent success). Methods provide a read-only view
|
||||
// of the same data with no meaningful performance difference — trivial getters
|
||||
// are inlined by the compiler.
|
||||
type Expect[T any] struct {
|
||||
value T
|
||||
err error
|
||||
@ -49,9 +55,26 @@ func Err[T any](err error) Expect[T] {
|
||||
}
|
||||
|
||||
// Errf wraps a formatted error in an Expect. It is a convenience shorthand
|
||||
// for [Err][fmt.Errorf(format, args...)].
|
||||
// for [Err][fmt.Errorf(format, args...)]. The caller's file and line are
|
||||
// prepended to the error message automatically.
|
||||
func Errf[T any](format string, args ...any) Expect[T] {
|
||||
return Expect[T]{err: fmt.Errorf(format, args...)}
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
loc := fmt.Sprintf("%s:%d", filepath.Base(file), line)
|
||||
return Expect[T]{err: fmt.Errorf(loc+": "+format, args...)}
|
||||
}
|
||||
|
||||
// Errw wraps an existing error with a context message, following the standard
|
||||
// Go error-propagation convention (errors.Is/As chain is preserved). Each
|
||||
// wrapping level is placed on its own line so the full error reads as a
|
||||
// top-down trace: outermost context first, root cause last. The caller's file
|
||||
// and line are prepended automatically.
|
||||
//
|
||||
// main.go:42: load config
|
||||
// logger.go:35: parse log level
|
||||
// strconv.Atoi: parsing "x": invalid syntax
|
||||
func Errw[T any](err error, format string, args ...any) Expect[T] {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
return Expect[T]{err: fmt.Errorf("%s:%d: %s\n%w", filepath.Base(file), line, fmt.Sprintf(format, args...), err)}
|
||||
}
|
||||
|
||||
// Of is a convenience constructor that bridges standard Go (value, error)
|
||||
@ -68,7 +91,9 @@ func (r Expect[T]) Err() error {
|
||||
}
|
||||
|
||||
// Value returns the underlying value, or the zero value of T if the Expect
|
||||
// holds an error. Always check [Expect.Err] first.
|
||||
// holds an error. Mirrors the (value, error) convention: the caller is trusted
|
||||
// to check [Expect.Err] first, just as they would check the error return in
|
||||
// standard Go code.
|
||||
func (r Expect[T]) Value() T {
|
||||
return r.value
|
||||
}
|
||||
@ -89,6 +114,81 @@ func (r Expect[T]) Unwrap() (T, error) {
|
||||
return r.value, r.err
|
||||
}
|
||||
|
||||
// Expect returns the value or exits the current goroutine with the wrapped
|
||||
// error annotated with msg. The failure is collected by the enclosing [Go] or
|
||||
// [Run] call as a normal Go error. A 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 {
|
||||
exitGoroutine(&stackError{
|
||||
err: fmt.Errorf("%s\n%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.
|
||||
//
|
||||
// data := Parse(raw).Expectf("parse user input id=%d", id)
|
||||
func (r Expect[T]) Expectf(format string, args ...any) T {
|
||||
if r.err != nil {
|
||||
exitGoroutine(&stackError{
|
||||
err: fmt.Errorf("%s\n%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 {
|
||||
ch <- Err[T](se) // Expect/Expectf/Must failure
|
||||
return
|
||||
}
|
||||
panic(v) // genuine runtime panic — crash the program
|
||||
}
|
||||
if finished {
|
||||
ch <- Ok(val)
|
||||
return
|
||||
}
|
||||
if err := collectGoexitFailure(); err != nil {
|
||||
ch <- Err[T](err)
|
||||
} else {
|
||||
ch <- Errf[T]("goroutine exited unexpectedly")
|
||||
}
|
||||
}()
|
||||
val = fn()
|
||||
finished = true
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Go runs fn in a new goroutine and blocks until it completes, returning the
|
||||
// result as Expect[T]. It is a convenience wrapper around [Async] for the
|
||||
// common single-goroutine case.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user