- 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
57 lines
1.8 KiB
Go
57 lines
1.8 KiB
Go
//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
|
||
}
|