From c737e7d6bebf1375a48da6d9f053f99ffd44a841 Mon Sep 17 00:00:00 2001 From: djmil Date: Mon, 4 May 2026 19:15:11 +0000 Subject: [PATCH] slides --- .devcontainer/devcontainer-lock.json | 9 ++ .devcontainer/devcontainer.json | 1 - docs/result-slides.md | 179 +++++++++++++++++++++++++++ pkg/result/bench_test.go | 7 +- 4 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 .devcontainer/devcontainer-lock.json create mode 100644 docs/result-slides.md diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..17e445a --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "version": "2.5.7", + "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:dbf431d6b42d55cde50fa1df75c7f7c3999a90cde6d73f7a7071174b3c3d0cc4", + "integrity": "sha256:dbf431d6b42d55cde50fa1df75c7f7c3999a90cde6d73f7a7071174b3c3d0cc4" + } + } +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 851e86e..4aa9ce9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -60,7 +60,6 @@ "forwardPorts": [8080], "remoteEnv": { - "CONFIG_PATH": "${containerWorkspaceFolder}/config/dev.yaml", "GOPRIVATE": "gitea.djmil.dev" } } diff --git a/docs/result-slides.md b/docs/result-slides.md new file mode 100644 index 0000000..18f2e59 --- /dev/null +++ b/docs/result-slides.md @@ -0,0 +1,179 @@ +--- +theme: gaia +highlightTheme: monokai +margin: 0 +html: true +marp: true +--- + +# `result` +## Happy-Path Error Handling in Go + +> Focus on success. Handle failure at the boundary. + + + +--- + +## The Problem + +```go +func process(raw string) (Output, error) { + h, err := parseHeader(raw) + if err != nil { return Output{}, err } + + f, err := validate(h) + if err != nil { return Output{}, err } + + r, err := transform(f) + if err != nil { return Output{}, err } + + return format(r) +} +``` + +- Half the function is error plumbing +- The actual logic is buried under boilerplate + + + +--- + +## The Idea + +```go +func process(raw string) error { + return result.Run(func() { + h := parseHeader(raw).Expect("parse header") + f := validate(h).Expect("validate") + r := transform(f).Expect("transform") + _ = format(r).Expect("format") + }) +} +``` + +- `Expect[T]` holds a value *or* an error — like a typed Result type +- `.Expect("context")` unwraps on success, exits on failure + + + +--- + +## How It Works + +```mermaid +flowchart LR + A[".Expect() sees error"] --> B["panic"] + B --> C["Run's deferred\nrecover()"] + C --> D["normal Go error"] +``` + +- `result.Run` spawns one goroutine and defers recovery +- Failure exits via `panic` — caught and returned as a plain `error` + + + +--- + +## The One Rule + +```go +// ✅ pkg/ library — returns Expect[T], never calls .Expect() +func Parse(raw string) result.Expect[Header] { + if raw == "" { + return result.Fail[Header](errors.New("empty input")) + } + return result.Ok(Header{raw}) +} + +// ✅ application code — calls .Expect() inside Run +h := parser.Parse(raw).Expect("parse header") +``` + +- Libraries **return** `Expect[T]` — let the caller decide +- Application code **chains** `.Expect()` inside `Run` + + + +--- + +## Defers Work Normally + +```go +result.Run(func() { + conn := pool.Acquire() + defer conn.Release() // ✅ always fires — success or failure + + score := compute(conn).Expect("compute score") + _ = save(score).Expect("save result") +}) +``` + +- Failure travels as a *value* — functions return normally +- Deferred cleanup fires before `.Expect()` triggers the exit + + + +--- + +## Debugging: Stack Traces + +``` +result.StackTrace(err): + + main.processRequest.func1 + /app/handler.go:42 + main.run.func1 + /app/main.go:17 +``` + +- Trace captured at the `.Expect()` call site — pinpoints the failure +- `result.CaptureStack = false` at startup for production builds + + + +--- + +## Two Build Modes + +| | Default | `-tags result_goexit` | +|--|--|--| +| Exit mechanism | `panic` | `runtime.Goexit` | +| Error path cost | ~1.4 µs | ~5.5 µs | +| Stray `recover()` safe | ⚠ re-panic unknowns | ✅ always safe | + +- **Panic** (default): 4× faster; standard Go `recover()` idiom applies +- **Goexit** (opt-in): immune to accidental error swallowing by any middleware + + + +--- + +## Performance + +| Scenario | Canonical Go | `result` | `result` (no trace) | +|--|--|--|--| +| Happy path | 1 324 ns | 1 046 ns | 1 027 ns | +| Error at depth 3 | 820 ns | 1 441 ns | 1 149 ns | +| Error at depth 10 | 1 257 ns | 1 576 ns | 1 113 ns | + +*arm64, both sides spawn one goroutine per boundary* + +- **Happy path**: result matches or beats canonical +- **Error path**: +300–600 ns — rounds to zero next to any I/O + + + +--- + +## Summary + +- Write functions for the happy path; handle errors once at the boundary +- `pkg/` returns `Expect[T]` — application code calls `.Expect()` inside `Run` +- Defers, concurrent patterns, and stack traces all work as expected +- Tune with `CaptureStack` and build tags if the profile demands it + +> **Best fit: application pipelines with 3+ sequential fallible steps.** + + diff --git a/pkg/result/bench_test.go b/pkg/result/bench_test.go index c7b0630..cb2afb0 100644 --- a/pkg/result/bench_test.go +++ b/pkg/result/bench_test.go @@ -391,10 +391,9 @@ func BenchmarkResult_HappyPath(b *testing.B) { // ── CaptureStack=false variants ──────────────────────────────────────────────── // -// These show the floor: goroutine spawn + Goexit mechanism with no stack -// capture. Set result.CaptureStack=false at startup to reach this level in -// production. The happy-path cost is unchanged (CaptureStack is only -// consulted on error paths). +// Shows the floor: goroutine spawn + Goexit mechanism with no stack capture. Set +// result.CaptureStack=false at startup to reach this level in production. The +// happy-path cost is unchanged (CaptureStack is only consulted on error paths). func BenchmarkResult_NoStack_HappyPath(b *testing.B) { result.CaptureStack = false