skip to content

Pointer Conversion Rules

unsafe.Pointer conversions are legal only in the patterns the package documents, and a uintptr held in a variable keeps nothing alive, so the collector can free memory an address still names.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

Why must a Go uintptr-to-unsafe.Pointer round trip with arithmetic happen in one expression?

level: middleimportance: must knowfreq 55%

answer

  1. one is scanned, the other is not
  2. stacks grow by being copied elsewhere
  3. nothing rewrites an integer
  4. liveness holds only within one expression
  5. and the result must stay inside the object

basics

~20 s

Go's garbage collector does not treat a uintptr as a reference. If the address rests in a variable between statements, the object can be collected, or moved when a goroutine stack is copied, so the address converted back is stale.

solid answer

~50 s

A `uintptr` is an integer, not a pointer: the collector does not scan it, does not keep the object it names alive, and does not rewrite it when a goroutine's stack grows and is copied to a larger allocation. Inside one expression the compiler can see that the `unsafe.Pointer` operand is still live and keeps the object anchored across the arithmetic. Split it across statements and that guarantee is gone — `u := uintptr(unsafe.Pointer(&x))` followed by `unsafe.Pointer(u + 8)` is explicitly listed as invalid, even though it compiles and usually appears to work. Two further conditions apply: the arithmetic may only be addition, subtraction or masking for alignment, and the result must still point inside the same allocated object, so computing an address just past the end is invalid. Prefer `unsafe.Add(p, off)`, which expresses the offset without ever producing a `uintptr` you could accidentally store.

code

go · 10 lines
go
// INVALID: u is a plain integer, so between the two statements rec
// may be collected, or moved by a goroutine stack copy.
u := uintptr(unsafe.Pointer(&rec))
bad := unsafe.Pointer(u + 8)

// VALID: one expression, with only arithmetic in between.
ok := unsafe.Pointer(uintptr(unsafe.Pointer(&rec)) + 8)

// VALID and clearer: no uintptr ever exists as a value.
best := unsafe.Add(unsafe.Pointer(&rec), 8)

go deeper

for a junior

Remember the shape of the rule even if you never write it: a uintptr is a number, and an address parked in a variable is no longer something the runtime tracks.

for a middle

Explain both failure modes precisely — collection because nothing reachable refers to the object, and relocation when a goroutine stack is copied to a larger one — and name unsafe.Add as the form that avoids both.

for a senior

Show how you would find this in a codebase you inherited: go vet's unsafeptr check, tests under -race so checkptr instrumentation is active, and a review rule that any bare uintptr local is suspect.

for a principal

Frame it as a review-surface question: the rule is unenforceable by eye at scale, so decide which packages may do pointer arithmetic at all and require unsafe.Add plus a checkptr-enabled test run in those packages.

## Two words that look alike and are not `unsafe.Pointer` is a pointer as far as the runtime is concerned. It sits in the pointer maps the compiler emits for stack frames and heap objects, the collector follows it when marking, and the runtime rewrites it when it relocates the memory it points into. `uintptr` is an unsigned integer wide enough to hold an address. The runtime treats it as data. Nothing about a `uintptr` is special except its name. That single difference produces the whole rule. ## What can happen between two statements ``` u := uintptr(unsafe.Pointer(&x)) p := unsafe.Pointer(u + 8) ``` Between those two lines, two things may happen to `x`. **It may be collected.** If `u` is the only thing left holding the address, `x` is unreachable the moment the conversion happens, because the collector does not count integers as roots. A collection cycle can run between the statements and reclaim it. The address in `u` then names free memory that a later allocation will reuse. **It may move.** Goroutines start with small stacks and grow them by allocating a bigger one and copying the old frames across. Every real pointer into the old stack is found and rewritten during that copy — every `*T`, every `unsafe.Pointer`, in registers, in locals, in heap objects. A `uintptr` is skipped, because the runtime has no way to tell an address from an ordinary number. The stack can grow on any function call, including one the compiler inlines into the line between your two statements. Note that Go's heap collector is non-moving today, but stack copying alone is enough to break this code, and depending on it staying that way is not a bet worth making. ## Why one expression is different Written as a single expression: ``` p := unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + 8) ``` the compiler can see the original `unsafe.Pointer` operand as part of the value being computed, and it keeps the referenced object live and anchored for the duration of that expression. There is no point at which the only surviving trace of the object is an integer stored in a variable the collector will read as data. This is not a stylistic preference in the documentation; it is the boundary of what the implementation promises. ## The rest of the rule The documented pattern has more to it than the single-expression requirement. - **Only arithmetic in between.** Addition and subtraction of an offset, and `&^` masking to round an address for alignment. A function call in the middle is not arithmetic and breaks the pattern. - **The result must stay inside the same allocated object.** Advancing through a struct or an array element by element is fine. Computing an address one past the end — the classic loop-termination trick from C — is listed as invalid, because that address is outside the object and the collector may associate it with something else entirely. - **You cannot invent an address.** Converting an arbitrary integer to `unsafe.Pointer` and dereferencing it is not one of the patterns; the number has to have come from a real pointer into a live object. There are a couple of narrow extra patterns worth recognising even if you never write them: a `uintptr(unsafe.Pointer(p))` conversion written directly in the argument list of a `syscall.Syscall` call is special-cased by the compiler so the object stays alive across the call, and the `uintptr` returned by `reflect.Value.Pointer` or `reflect.Value.UnsafeAddr` must be converted back to `Pointer` in the same expression that produced it, for exactly the same reason as above. ## Write unsafe.Add instead Modern Go gives you `unsafe.Add(ptr Pointer, len IntegerType) Pointer`, which offsets a pointer without a `uintptr` ever existing as a value you could store. It is shorter, it removes the possibility of the split-statement bug, and it makes review trivial: if a diff contains a bare `uintptr` local, that is the thing to question. ## How this fails in practice The bad version usually works. Objects are often reachable through something else, and a stack copy at exactly the wrong instruction is rare, so tests pass and the code ships. It fails later, under load, in a way that looks like memory corruption rather than a pointer bug. Building with `-race` turns on the compiler's checkptr instrumentation, which validates `unsafe.Pointer` conversions at runtime and reports arithmetic that lands outside a valid allocation; `go vet`'s `unsafeptr` check catches many of the split-statement forms statically. Run both against any package that does this arithmetic.

  • Go's heap collector does not move objects, so why does the moving argument matter?
    Because goroutine stacks do move. They start small and grow by allocating a larger stack and copying the frames, rewriting every pointer found during the copy — a `uintptr` is not found. Beyond that, the rule is written against what the implementation promises, not against what today's collector happens to do.
  • Which operations are allowed between the two conversions?
    Adding or subtracting an offset, and masking with `&^` to round an address down for alignment. Nothing else — a function call in the middle already breaks the pattern. The result must also still point inside the same allocated object, which rules out computing an address one past the end.
  • How would you catch a violation of this rule in CI?
    Run `go vet`, whose `unsafeptr` check flags likely invalid `uintptr`-to-`Pointer` conversions statically, and run the package's tests with `-race`, which enables the compiler's checkptr instrumentation so bad conversions and out-of-object arithmetic abort at runtime rather than corrupting memory quietly.
  • Is unsafe.Add always equivalent to the single-expression uintptr form?
    For offsetting a pointer, yes, and it is the form to prefer because no `uintptr` value ever exists to be stored by accident. The uintptr form is still needed for alignment masking with `&^`, which `unsafe.Add` does not express.

Holding a uintptr is like writing down a hotel room number instead of keeping the key: the guest can check out or be moved to another floor between the moment you write it and the moment you walk to the door.

saying these in an interview costs you the question

  • Says it is only a style rule and the split version works
  • Claims Go never moves memory so nothing can go stale
  • Thinks a local uintptr keeps its object reachable
  • Believes any arithmetic result is fine if it is not dereferenced
  • Computes an address one past the end of an object
open as a page

What is Go's unsafe.Pointer, and which pointer conversions does it make legal?

level: juniorimportance: should knowfreq 35%

basics

~20 s

unsafe.Pointer is a pointer type with no element type. Any typed pointer converts to it and back out as a different typed pointer, and it converts to and from uintptr. It is Go's only bridge between unrelated pointer types.

open as a page

What must hold before you convert &buf[off] of an mmap'd Go []byte into a *record?

level: middleimportance: should knowfreq 30%

basics

~20 s

The address must satisfy the record type's alignment, the whole struct must fit inside the mapped region from that offset, and the struct's field layout with its padding must match the on-disk record exactly. Otherwise the conversion is undefined.

open as a page

A Go library caches record addresses in a map[uint64]uintptr and crashes randomly; how do you diagnose and fix it?

level: seniorimportance: should knowfreq 35%

basics

~10 s

A uintptr is not a reference, so once the cache holds the only copy of an address the record can be collected and its memory reused. Store a typed pointer or unsafe.Pointer instead.

open as a page

As Go platform owner, how do you decide which packages may import unsafe, and what do you demand for an exception?

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Default to a ban enforced in CI, with a short allowlist of package paths each having a named owner. Grant exceptions only against a benchmark the safe version cannot match, with conversions confined to one small package.

open as a page