//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 }