Upstream Go and Closing Thoughts

Originally published in Japanese at https://zenn.dev/ymotongpoo/books/go-ebpf-primer/viewer/65-conclusion.

The four hurdles in this book were all difficulties of looking into Go’s internals from outside.

Direct support for eBPF instrumentation

Upstream Go provides little direct support for eBPF instrumentation. The uretprobe problem from Hurdle 1 (#22008) has remained “Unplanned” since it was reported in 2017. A proposal to allow hooks on goroutine creation (#73798) was closed as “not planned” in 2025.

The Go team takes a consistent stance: runtime internals are not a public API, and external code should not depend on them. eBPF instrumentation depends on exactly those private internals, so direct support is hard to come by. The cost of tracking offsets, covered in Hurdle 3, is the flip side of that same stance. Tracking mainly targets private fields in the standard library and third-party libraries rather than the runtime itself, but the policy of keeping internal structures out of the stable API holds for both the runtime and its libraries.

The decision not to expose goid follows the same reasoning. In Hurdle 2, OBI chose the address of g as its identifier because avoiding an unexposed value is less fragile than trying to read it. The external tool accommodates Go’s policy by designing around the value instead of depending on it.

The evolution of observability “from the inside”

Go has added several observability mechanisms that operate from inside the application or runtime.

  • Flight recording (#63185, implemented in Go 1.25 as runtime/trace.FlightRecorder): keeps the most recent execution trace in a ring buffer so you can capture the events leading up to a problem when it occurs.
  • goroutine leak profile: a profile of goroutines that have become unreachable yet remain blocked, added to runtime/pprof in Go 1.26 under the name goroutineleak. An experimental feature enabled by building with GOEXPERIMENT=goroutineleakprofile.
  • Compile-Time Instrumentation SIG (formed in January 2025): an approach separate from eBPF that embeds instrumentation code at compile time. It reached its first stable release in 2026.

eBPF observes from outside the process without changing the application, but it must keep tracking changes to internal structures. Go’s internal mechanisms are accurate and hard to break, but they require changes to the code or build process.

Observation from the outside and from the inside Figure 1: The arrows point from the observer to the observed. The labels show the benefit and the cost of each approach.

Closing thoughts

The four hurdles connect Go traits to OBI’s implementation choices.

HurdleGo trait it collides withOBI’s answer
1. uretprobe doesn’t workMovable stacksOrdinary uprobes on every RET instruction
2. Register ABIABIInternal (the argument-passing convention, Go 1.17+)Per-architecture register mapping tables; goroutines identified by the address of g, contents never read
3. Version-dependent offsetsPrivate internals of libraries and the runtimeRead the binary’s DWARF, fill the gaps from the auto-updated offsets.json
4. Context propagationgoroutines (≠ threads)Parent-child tracking at newproc1 (up to 6 levels), plus direct writes to bufio.Writer or sk_msg

These four problems are not separate accidents; each is the flip side of a Go strength: movable stacks, a custom register ABI, private internal structures, and goroutines. The same design that makes Go fast and easy to write is what blocks anyone observing it from outside.

These hurdles also explain application-level failures. Go versions and dependency versions determine which offsets OBI knows, while build flags determine whether it can read those offsets from DWARF. Stripping DWARF forces all field lookups through the precomputed table.

The Chapter 1 claim that eBPF works “regardless of language” applies to OBI’s generic protocol path. Its Go function path scans RET instructions, reads architecture-specific registers, combines DWARF with a versioned offset table, and writes trace context into a bufio.Writer buffer. Those mechanisms make observation possible without changing application code.

The balance between observing from outside and observing from inside is still shifting.