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,
|
"go.testExplorer.enable": true,
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"djmil",
|
"djmil",
|
||||||
|
"Errf",
|
||||||
|
"Errw",
|
||||||
"Expectf",
|
"Expectf",
|
||||||
"Failf",
|
"Failf",
|
||||||
"gitea",
|
"gitea",
|
||||||
"golangci",
|
"golangci",
|
||||||
"nolint",
|
"nolint",
|
||||||
"testutil",
|
"testutil"
|
||||||
"Errf"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := result.Run(showGreeting); err != nil {
|
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 != "" {
|
if stack := result.StackTrace(err); stack != "" {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", 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.
|
// Greet returns a personalized greeting and logs the interaction.
|
||||||
func (s *Service) Greet(name string) result.Expect[string] {
|
func (s *Service) Greet(name string) result.Expect[string] {
|
||||||
if name == "" {
|
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)
|
msg := fmt.Sprintf("Hello, %s!", name)
|
||||||
|
|||||||
@ -32,7 +32,7 @@ type Logger struct {
|
|||||||
func New(level string) result.Expect[*Logger] {
|
func New(level string) result.Expect[*Logger] {
|
||||||
lvl := parseLevel(level)
|
lvl := parseLevel(level)
|
||||||
if lvl.Err() != nil {
|
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()})
|
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] {
|
func parseLevel(level string) result.Expect[slog.Level] {
|
||||||
var lvl slog.Level
|
var lvl slog.Level
|
||||||
if err := lvl.UnmarshalText([]byte(level)); err != nil {
|
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)
|
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
|
// Expect[T] is a drop-in replacement for (T, error) that also enables
|
||||||
// application logic. Instead of propagating (value, error) pairs through every
|
// panic-based happy-path propagation when that suits the code better. Both
|
||||||
// frame, functions return Expect[T] and the caller unwraps at the boundary.
|
// 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
|
// # Layering rule
|
||||||
//
|
//
|
||||||
// Reusable library code (packages under pkg/) must only *return* Expect[T] —
|
// Reusable library code (packages under pkg/) must only *return* Expect[T] —
|
||||||
// it must never call .Expect(), .Must(), or .Expectf() itself. Those methods
|
// it must never call .Expect(), .Must(), or .Expectf() itself. Those methods
|
||||||
// exit the current goroutine via runtime.Goexit and are only safe inside a
|
// exit the current goroutine and are only safe inside a goroutine controlled
|
||||||
// goroutine controlled by [Go] or [Run]. Calling them in a library takes
|
// by [Go] or [Run].
|
||||||
// control away from the caller and makes the package non-composable.
|
|
||||||
//
|
//
|
||||||
// The right split:
|
// 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,
|
// - Application code (cmd/, HTTP handlers, …): chain .Expect() calls freely,
|
||||||
// protected by a defer result.Catch(&err) or a result.Run wrapper.
|
// 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
|
// Stack traces are captured at the failure site and can be retrieved from the
|
||||||
// collected error via [StackTrace].
|
// collected error via [StackTrace].
|
||||||
//
|
//
|
||||||
// # Constructors
|
// # Constructors
|
||||||
//
|
//
|
||||||
// Use [Ok] to wrap a success value, [Err] to wrap an error, and [Of] to
|
// Use [Ok] to wrap a success value, [Err] / [Errf] / [Errw] to wrap errors,
|
||||||
// bridge existing (value, error) return signatures:
|
// and [Of] to bridge existing (value, error) return signatures:
|
||||||
//
|
//
|
||||||
// data := result.Of(os.ReadFile("cfg.json")).Expect("read config")
|
// data := result.Of(os.ReadFile("cfg.json")).Expect("read config")
|
||||||
//
|
//
|
||||||
@ -43,8 +63,8 @@
|
|||||||
//
|
//
|
||||||
// func run() error {
|
// func run() error {
|
||||||
// return result.Run(func() {
|
// return result.Run(func() {
|
||||||
// port := parsePort(cfg.Port).Expect("load config port")
|
// host := parseHost(cfg.Host).Expect("load config host")
|
||||||
// _ = port // happy path continues …
|
// _ = host // happy path continues …
|
||||||
// })
|
// })
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
@ -55,8 +75,8 @@
|
|||||||
//
|
//
|
||||||
// func load() (err error) {
|
// func load() (err error) {
|
||||||
// defer result.Catch(&err)
|
// defer result.Catch(&err)
|
||||||
// port := parsePort(cfg.Port).Expect("load config port")
|
// host := parseHost(cfg.Host).Expect("load config host")
|
||||||
// _ = port
|
// _ = host
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
|
|||||||
@ -19,7 +19,7 @@ func parseHost(s string) result.Expect[string] {
|
|||||||
func parsePort(s string) result.Expect[int] {
|
func parsePort(s string) result.Expect[int] {
|
||||||
port := result.Of(strconv.Atoi(s))
|
port := result.Of(strconv.Atoi(s))
|
||||||
if port.Err() != nil {
|
if port.Err() != nil {
|
||||||
return port
|
return result.Errw[int](port.Err(), "result.Of")
|
||||||
}
|
}
|
||||||
if port.Value() < 1 || port.Value() > 65535 {
|
if port.Value() < 1 || port.Value() > 65535 {
|
||||||
return result.Errf[int]("%d out of range", port.Value())
|
return result.Errf[int]("%d out of range", port.Value())
|
||||||
@ -27,15 +27,23 @@ func parsePort(s string) result.Expect[int] {
|
|||||||
return port
|
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
|
// Example_errCheck shows checking the error without panicking — useful at the
|
||||||
// outermost boundary where you want a normal error return.
|
// outermost boundary where you want a normal error return.
|
||||||
func Example_errCheck() {
|
func Example_errCheck() {
|
||||||
r := parsePort("not-a-number")
|
r := parsePort("not-a-number")
|
||||||
if r.Err() != nil {
|
if r.Err() != nil {
|
||||||
fmt.Println("parsePort failed:", r.Err())
|
fmt.Println("failed:", r.Err())
|
||||||
}
|
}
|
||||||
// Output:
|
// 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
|
// Example_happyPath shows the basic happy-path pattern: call Expect at each
|
||||||
@ -59,16 +67,12 @@ func Example_catch() {
|
|||||||
|
|
||||||
if err := loadHost(); err != nil {
|
if err := loadHost(); err != nil {
|
||||||
fmt.Println("caught:", err)
|
fmt.Println("caught:", err)
|
||||||
|
// NOTE: you can have stack trace of an error as well
|
||||||
|
// fmt.Println("stacktrace:", result.StackTrace(err))
|
||||||
}
|
}
|
||||||
// Output:
|
// Output:
|
||||||
// caught: read host: host must not be empty
|
// caught: read host
|
||||||
}
|
// example_test.go:13: 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example_run shows result.Run as a lightweight synchronous boundary — a concise
|
// Example_run shows result.Run as a lightweight synchronous boundary — a concise
|
||||||
@ -101,7 +105,8 @@ func Example_runtimeError() {
|
|||||||
fmt.Println("caught:", err)
|
fmt.Println("caught:", err)
|
||||||
}
|
}
|
||||||
// Output:
|
// 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
|
// 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() {
|
func Example_go() {
|
||||||
url := result.Go(func() string {
|
url := result.Go(func() string {
|
||||||
return getURL("localhost", "8080")
|
return getURL("localhost", "8080")
|
||||||
})
|
}).Must()
|
||||||
|
|
||||||
fmt.Println(url.Expect("get url"))
|
fmt.Println(url)
|
||||||
// Output:
|
// Output:
|
||||||
// http://localhost:8080
|
// http://localhost:8080
|
||||||
}
|
}
|
||||||
@ -127,7 +132,8 @@ func Example_goError() {
|
|||||||
fmt.Println("failed:", err)
|
fmt.Println("failed:", err)
|
||||||
}
|
}
|
||||||
// Output:
|
// 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
|
// Example_unwrap shows re-joining the normal Go (value, error) world at a
|
||||||
|
|||||||
@ -15,13 +15,12 @@
|
|||||||
package result
|
package result
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// gErrors stores errors set by Expect/Expectf before calling runtime.Goexit,
|
// gErrors stores errors set by exitGoroutine before calling runtime.Goexit,
|
||||||
// keyed by goroutine ID. Entries are consumed by the enclosing Go or Run call.
|
// keyed by goroutine ID. Entries are consumed by the enclosing Async call.
|
||||||
var gErrors sync.Map
|
var gErrors sync.Map
|
||||||
|
|
||||||
// goroutineID returns the current goroutine's numeric ID by parsing the first
|
// goroutineID returns the current goroutine's numeric ID by parsing the first
|
||||||
@ -40,80 +39,18 @@ func goroutineID() uint64 {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expect returns the value or exits the current goroutine via runtime.Goexit,
|
// exitGoroutine stores se in gErrors and exits the goroutine via Goexit.
|
||||||
// storing the annotated error for collection by the enclosing [Go] or [Run]
|
// The stored error is retrieved by collectGoexitFailure in the enclosing Async.
|
||||||
// call. The stack trace is captured at this call site.
|
func exitGoroutine(se *stackError) {
|
||||||
//
|
gErrors.Store(goroutineID(), se)
|
||||||
// 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()
|
runtime.Goexit()
|
||||||
}
|
|
||||||
return r.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expectf is like [Expect.Expect] but accepts a fmt.Sprintf-style format string
|
// collectGoexitFailure retrieves any failure stored by exitGoroutine for the
|
||||||
// for the context message. The wrapped error is always appended as ": <err>".
|
// current goroutine, consuming the entry.
|
||||||
//
|
func collectGoexitFailure() error {
|
||||||
// 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 {
|
if stored, ok := gErrors.LoadAndDelete(goroutineID()); ok {
|
||||||
ch <- Err[T](stored.(error))
|
return stored.(error)
|
||||||
} else {
|
|
||||||
ch <- Errf[T]("goroutine exited unexpectedly")
|
|
||||||
}
|
}
|
||||||
}()
|
return nil
|
||||||
val = fn()
|
|
||||||
finished = true
|
|
||||||
}()
|
|
||||||
return ch
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,77 +28,10 @@
|
|||||||
|
|
||||||
package result
|
package result
|
||||||
|
|
||||||
import (
|
// exitGoroutine signals a result failure by panicking with se.
|
||||||
"fmt"
|
// 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
|
// collectGoexitFailure is a no-op in the panic build — failures travel via
|
||||||
// collected by the enclosing [Go] or [Run] call. The stack trace is captured
|
// panic/recover, not via the gErrors map.
|
||||||
// at this call site when [CaptureStack] is true.
|
func collectGoexitFailure() error { return nil }
|
||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ func TestMustCollected(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
if err.Error() != "unrecoverable" {
|
if !strings.Contains(err.Error(), "unrecoverable") {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,17 @@ package result
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Expect holds either a value of type T or an error.
|
// 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 {
|
type Expect[T any] struct {
|
||||||
value T
|
value T
|
||||||
err error
|
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
|
// 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] {
|
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)
|
// 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
|
// 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 {
|
func (r Expect[T]) Value() T {
|
||||||
return r.value
|
return r.value
|
||||||
}
|
}
|
||||||
@ -89,6 +114,81 @@ func (r Expect[T]) Unwrap() (T, error) {
|
|||||||
return r.value, r.err
|
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
|
// 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
|
// result as Expect[T]. It is a convenience wrapper around [Async] for the
|
||||||
// common single-goroutine case.
|
// common single-goroutine case.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user