Go maintains a concurrent garbage collector that must identify all live pointers to determine which heap objects remain reachable. Unlike C, Go treats uintptr as an opaque integer type that carries no pointer metadata, meaning the garbage collector ignores values of this type during root scanning and pointer traversal. This design allows integer arithmetic on addresses but creates a hazardous gap where valid memory references can appear as mere numbers, invisible to the runtime's liveness tracking.
When developers perform address calculations—such as accessing array elements without bounds checks or aligning memory—they often convert an unsafe.Pointer to uintptr, apply offsets, then convert back. If these steps occur across multiple statements or function calls, the intermediate uintptr value becomes the sole evidence of the memory reference. The garbage collector, seeing no pointer, may conclude the underlying object is unreachable and reclaim it, leading to use-after-free crashes or data corruption when the final pointer conversion attempts to access the now-invalid memory.
Go mandates that any conversion from unsafe.Pointer to uintptr and back must occur within the same expression, without intermediate storage or function calls. This pattern ensures the compiler keeps the original pointer live throughout the arithmetic operation, preventing concurrent garbage collection cycles from reclaiming the referenced object. The canonical form is (*T)(unsafe.Pointer(uintptr(p) + offset)), where the entire calculation remains a single evaluation.
A high-throughput packet processing system needed to parse protocol headers directly from a byte slice without Go's bounds-checking overhead. The engineering team required accessing the 8th byte of a 1500-byte MTU buffer using pointer arithmetic to squeeze nanoseconds from the hot path and meet strict 10Gbps line-rate throughput requirements.
One approach involved storing the intermediate address calculation in a local variable for clarity: calculating addr := uintptr(unsafe.Pointer(&buf[0])) + 8, then later dereferencing *(*uint64)(unsafe.Pointer(addr)). While this improved readability and allowed breakpoint debugging of the address value, it introduced a fatal race condition—the garbage collector could run between the assignment and dereference, migrate the buffer to a new heap location, and render addr a dangling reference to the old address, causing segmentation violations or data corruption.
An alternative strategy wrapped the arithmetic in a helper function taking unsafe.Pointer and offset, performing the cast inside that function. However, because function calls act as scheduling points and may trigger stack growth or garbage collection, passing the pointer through function arguments did not guarantee the compiler would maintain the liveness of the original pointer throughout the helper's execution, still exposing the code to premature collection.
The team selected the single-expression pattern *(*uint64)(unsafe.Pointer(uintptr(unsafe.Pointer(&buf[0])) + 8)) encapsulated within a //go:nosplit assembly-style wrapper. This ensured the pointer arithmetic occurred atomically from the runtime's perspective, preventing the garbage collector from observing the intermediate uintptr state. The solution sacrificed some debuggability for correctness, using extensive unit tests and checkptr-enabled builds during CI to catch invalid conversions.
The packet processor achieved zero-allocation hot paths with stable sub-microsecond latency. No garbage collector-related crashes occurred in production, validated by running the service under GODEBUG=checkptr=1 during stress testing to verify no unsafe.Pointer violations escaped detection.
Why does converting unsafe.Pointer to uintptr and storing it in a variable before converting back violate Go's memory safety guarantees?
The Go garbage collector runs concurrently and can trigger at any allocation point. When you store the uintptr in a variable, you create a window where the object is referenced only by an integer. Because uintptr values are not scanned as roots, the GC may reclaim the object during this window, causing the subsequent pointer conversion to access freed memory.
How does the checkptr flag interact with unsafe.Pointer arithmetic, and why might valid code still trigger panics under GODEBUG=checkptr=2?
checkptr instrumentation validates that unsafe.Pointer conversions respect alignment and allocation bounds. Under checkptr=2, the compiler inserts runtime checks verifying that arithmetic stays within the original object. Valid code may panic if the arithmetic produces a pointer to the middle of an object or derives from a multi-statement uintptr calculation, as checkptr cannot verify the liveness guarantees across statement boundaries.
What is the difference between unsafe.Pointer rules and cgo pointer passing rules regarding transient pointers, and when can violating these cause Go to crash during stack growth?
While unsafe.Pointer requires atomic conversions, cgo imposes additional restrictions requiring pointers passed to C to remain pinned. Candidates often assume that storing Go pointers as uintptr in C memory is safe, but during Go stack growth or GC, these pointers may become invalid. The solution requires using runtime.Pinner or ensuring C calls complete before returning to Go, maintaining reachability invariants throughout the foreign function execution.