# Go - Portability and Crash Handling Go code that passes on amd64/macOS fails on 386, mips, and Windows for predictable reasons: hardcoded word widths, signal numbers that diverge along GOARCH, untyped constants that do not fit a 32-bit `int`, and CRLF checkouts. Crash handling adds lifecycle traps: leaked monitor children, `init()` functions that claim the monitor role, deferred stops that unwire the crash pipe before the traceback is written, and dump parsers that break when the runtime print format changes. ## Key points - Derive compile-time struct-size assertions from the type itself: `unsafe.Offsetof(vals) + unsafe.Sizeof([numAttrs]string{})`. A hardcoded byte count such as 56 holds only where `string` is 16 bytes. On 386 the struct is 28 bytes and the array-index guard underflows at compile time. The derived form asserts the real invariant, no trailing padding, on every word width. - Clamp a length that must fit `int` on 32-bit: `length = int(min(uint64(length), math.MaxUint32))` (Go 1.21+ `min` builtin). The semantics stay identical on 64-bit, where the clamp is unreachable. Go refuses to materialize the untyped constant `4294967295` into a 32-bit `int` at compile time. That refusal is the actual failure when a dependency carries no build tags for the unsupported path. - Signal numbers diverge along GOARCH, not only GOOS. `linux/mips` has SIGBUS 10 while `GOOS == "linux"`, so a `GOOS == "darwin"` check is structurally incapable of being right. Use real `syscall.SIG*` constants behind build tags. Keep POSIX literals only for Windows fixture parsing. - A transitive dependency that does not compile on 386 makes a whole-tree cross-compile guard fail, regardless of fixes in your own packages. Compute the 386-safe package set first: exclude importers of the broken library and test-only packages with a `go list` filter over import paths, Go files, and deps. Cross-compile only that set. - Windows checkouts carry CRLF. Line-anchored regexes with a literal `\n` match nothing on `windows-latest` and still pass on macOS. Test the real read path with a CRLF file on every platform. Make sure the regression test does not normalize the input before it matches. - Go map iteration is non-deterministic. An error message that formats a map's keys produces different output across runs unless the code sorts the keys first. A Prometheus duplicate-series error message showed this in practice. - `cmd.Start()` without `cmd.Wait()` leaks the monitor child as a zombie and holds its file descriptors open. The stdlib doc states that `Wait` must be called. Wait on every monitor process. - A dependency's `init()` runs before the importing package's `init()`. A package-level `init()` in the library can claim the monitor role before user side effects fire in the re-exec'd child. Init ordering is the mechanism, not a race. - `defer Stop()` silently kills capture of unrecovered panics. Go runs deferred functions during unwinding before the runtime writes the traceback, so `defer Stop()` unwires the crash pipe first. The monitor sees EOF and takes the clean-exit path. An end-to-end suite with full patch coverage missed this because no test combined `defer Stop()` with a crash. - The extended Go 1.26 crash-dump header `goroutine 1 gp=0x... m=0 mp=0x... [running]:` breaks older parsers completely: zero goroutines parse. A tolerant `[^[]*` header regex already handles it. Verify a parser on real fixtures, not by reading its regexes. - The Windows SEH crash header prints `Exception <code> <flag> <faulting address> <PC>` followed by a separate `PC=` line. The fields follow the runtime's print order (exception code, read/write/DEP flag, faulting address, register-context PC), not the `EXCEPTION_RECORD` struct layout. ## How to apply 1. Derive size expectations with `unsafe.Offsetof(vals) + unsafe.Sizeof([numAttrs]string{})`. Never hardcode a byte count. 2. Clamp lengths with `int(min(uint64(length), math.MaxUint32))` before they reach a 32-bit `int`. 3. Express signal checks with `syscall.SIG*` constants behind build tags. 4. When a dependency does not compile on 386, guard the 386-safe package set instead of `./...`: ```sh go list -f '{{.ImportPath}}|{{if .GoFiles}}Y{{end}}|{{join .Deps " "}}' ./... \ | awk -F'|' '$2=="Y" && $3 !~ /broken-lib-import-path/ {print $1}' \ | xargs env GOOS=linux GOARCH=386 CGO_ENABLED=0 go build ``` Substitute the import path of the broken library. 5. Test the read path with a CRLF file on every platform. Do not normalize before matching. 6. Call `cmd.Wait()` for every `cmd.Start()`. 7. Stop the crash-tracker explicitly on paths that can panic unrecovered. Do not use `defer Stop()` there. 8. Run dump parsers on real fixtures from each Go version, including Go 1.26 and Windows SEH dumps. ## Observations - [go-runtime] Map iteration is non-deterministic. Sort keys before formatting them into an error message. #go (source: agent sessions, 2026-06) - [portability] Hardcoded struct-size guards underflow on 386. Derive the expectation from the type. #go (source: agent sessions, 2026-07) - [portability] Clamp lengths with `int(min(uint64(length), math.MaxUint32))` for 32-bit safety. #go (source: agent sessions, 2026-07) - [portability] Signal numbers diverge along GOARCH. Use `syscall.SIG*` constants behind build tags. #go (source: agent sessions, 2026-07) - [ci] A broken 386 dependency forces a filtered cross-compile guard. #go (source: agent sessions, 2026-07) - [crash-handling] `cmd.Start()` needs `cmd.Wait()`, or the monitor leaks. #go (source: agent sessions, 2026-07) - [crash-handling] Dependency `init()` ordering can claim the monitor role first. #go (source: agent sessions, 2026-07) - [crash-handling] `defer Stop()` unwires the crash pipe before the traceback. Do not defer it on crash paths. #go (source: agent sessions, 2026-07) - [crash-handling] The Windows SEH header follows the runtime print order, not the struct layout. #go (source: agent sessions, 2026-07) - [portability] Windows checkouts carry CRLF. Regexes with a literal `\n` match nothing. #go (source: agent sessions, 2026-09) - [crash-handling] The Go 1.26 goroutine header breaks strict parsers. A `[^[]*` regex tolerates it. #go (source: agent sessions, 2026-09) ## Relations - relates_to [[resources/documentation/Go/Go - Internals]] (runtime print formats and dump parsing) - relates_to [[resources/documentation/Go/Go - Testing Patterns and Tests That Certify Nothing]] (vacuous regression tests and coverage gaps) - relates_to [[resources/documentation/CI - Failure Triage and Silent Non-Runs]] (the filtered cross-compile guard is a CI gate) - relates_to [[resources/documentation/Make - Portability Traps]] (portability traps outside Go) ## Sources Personal agent-session notes, June 2026 to September 2026