118 lines
4.2 KiB
Go
118 lines
4.2 KiB
Go
// Package result provides a generic Expect[T] type that supports two error
|
|
// handling styles without forcing either one.
|
|
//
|
|
// # Two modes, one type
|
|
//
|
|
// 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
|
|
//
|
|
// The rule is simple: .Expect() is safe anywhere a boundary ([Go] or [Run])
|
|
// owns the goroutine. In practice:
|
|
//
|
|
// - pkg/ functions that just compute and return: return Expect[T], let the
|
|
// caller decide how to handle it.
|
|
// - pkg/ functions that internally spawn goroutines via [Go] or [Run]: they
|
|
// own those goroutines and may freely chain .Expect() inside them. From
|
|
// the outside they still look like normal functions returning Expect[T].
|
|
// - Application code (cmd/, HTTP handlers, …): chain .Expect() freely,
|
|
// protected by a [Run] wrapper or defer [Catch].
|
|
//
|
|
// 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] / [Errf] / [Errw] to wrap errors,
|
|
// and [Of] to bridge existing (value, error) return signatures:
|
|
//
|
|
// data := result.Of(os.ReadFile("cfg.json")).Expect("read config")
|
|
//
|
|
// # Boundary pattern
|
|
//
|
|
// func run() error {
|
|
// return result.Run(func() {
|
|
// host := parseHost(cfg.Host).Expect("load config host")
|
|
// _ = host // happy path continues …
|
|
// })
|
|
// }
|
|
//
|
|
// [Go] is the typed variant — it returns Expect[T] when the closure produces
|
|
// a value. [Run] is a convenience wrapper for closures that return nothing.
|
|
//
|
|
// [Catch] is an alternative boundary for use with named error returns:
|
|
//
|
|
// func load() (err error) {
|
|
// defer result.Catch(&err)
|
|
// host := parseHost(cfg.Host).Expect("load config host")
|
|
// _ = host
|
|
// return
|
|
// }
|
|
//
|
|
// Important: [Catch] relies on recover() and only works with the default
|
|
// (panic) build. With -tags result_goexit, Expect and Expectf exit via
|
|
// runtime.Goexit which recover() cannot intercept — use [Run] or [Go] instead,
|
|
// as they work correctly in both builds.
|
|
//
|
|
// # Concurrent pattern
|
|
//
|
|
// Combining [Async] with the boundary pattern makes concurrent code almost as
|
|
// readable as sequential code. Fire goroutines with [Async], then collect with
|
|
// [All] or by reading channels individually — failures surface as normal errors
|
|
// at the boundary, with no manual WaitGroups, mutex guards, or error channels:
|
|
//
|
|
// func fetchAll(urls []string) ([]string, error) {
|
|
// return result.Map(urls, func(url string) string {
|
|
// return fetch(url).Expect("fetch") // happy path inside the goroutine
|
|
// }).Unwrap()
|
|
// }
|
|
//
|
|
// For heterogeneous concurrent work use [AsyncOf] — it accepts functions that
|
|
// already return Expect[T] (as library functions should), so only one .Expect()
|
|
// per goroutine is needed at the collection site:
|
|
//
|
|
// func loadConfig() (Config, error) {
|
|
// hostCh := result.AsyncOf(resolveHost) // resolveHost() Expect[string]
|
|
// portCh := result.AsyncOf(resolvePort) // resolvePort() Expect[int]
|
|
// return result.Go(func() Config {
|
|
// host := (<-hostCh).Expect("resolve host")
|
|
// port := (<-portCh).Expect("resolve port")
|
|
// return Config{Host: host, Port: port}
|
|
// }).Unwrap()
|
|
// }
|
|
//
|
|
// Genuine runtime panics (nil-pointer dereferences, index out of bounds, etc.)
|
|
// are not recovered — they still crash the program, as they should.
|
|
package result
|