template/pkg/result/expect_goexit.go
djmil 81f5a49cea 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
2026-05-06 23:36:14 +00:00

57 lines
1.8 KiB
Go
Raw Permalink 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
// Optional runtime.Goexit implementation, enabled with -tags result_goexit.
//
// Expect and Expectf exit the goroutine via runtime.Goexit instead of panic.
// Goexit runs all deferred functions but is invisible to recover() — no
// recover() call anywhere in the call chain can accidentally swallow a result
// failure, making this build safe to use alongside frameworks or user code
// that uses bare recover() inside Go/Run closures.
//
// Cost: error path ≈ 5.5 µs / ~536 B per failure (goroutineID parse +
// sync.Map store/load + Goexit unwind) — about 4× the default panic build.
// Happy-path cost is identical to the default.
package result
import (
"runtime"
"sync"
)
// 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
// line of runtime.Stack output ("goroutine NNN [...]"). Called only on error
// paths so the runtime.Stack overhead is acceptable.
func goroutineID() uint64 {
var buf [64]byte
n := runtime.Stack(buf[:], false)
var id uint64
for _, b := range buf[10:n] { // skip "goroutine "
if b < '0' || b > '9' {
break
}
id = id*10 + uint64(b-'0')
}
return id
}
// 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()
}
// collectGoexitFailure retrieves any failure stored by exitGoroutine for the
// current goroutine, consuming the entry.
func collectGoexitFailure() error {
if stored, ok := gErrors.LoadAndDelete(goroutineID()); ok {
return stored.(error)
}
return nil
}