skip to content

Raw Memory and Layout

unsafe.Pointer, Sizeof and unsafe.Slice let you step outside the type system for layout questions and zero-copy tricks, under rules the garbage collector will punish you for breaking.

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

explore

questions

13

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 does unsafe.Sizeof report for a Go string, slice or map value, and what does it not count?

level: middleimportance: must knowfreq 45%

basics

~20 s

unsafe.Sizeof counts only a value's own fixed representation: on a 64-bit platform 16 bytes for any string, 24 for any slice and 8 for any map. The bytes, elements and hash table those pointers reach are never counted.

open as a page

Why is writing through unsafe.StringData(s) to a string's bytes undefined rather than a hack that happens to work?

level: middleimportance: must knowfreq 40%

basics

~20 s

Go guarantees a string never changes, and the compiler, runtime and library all build on that. Literals may sit in read-only memory, copies share one array, and a mutated map key stays stranded under its old hash.

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 does Go's unsafe.Sizeof(x) return, and is it computed at run time or at compile time?

level: juniorimportance: should knowfreq 30%

basics

~20 s

unsafe.Sizeof(x) gives the size in bytes of x's own type representation. The compiler replaces the call with a uintptr constant, so no code runs for it and the result never depends on the contents of x.

open as a page

What does unsafe.String(unsafe.SliceData(b), len(b)) return for a []byte b, and what must you never do to b afterwards?

level: juniorimportance: should knowfreq 25%

basics

~20 s

It returns a string that reuses b's existing byte array instead of copying it, so the conversion allocates nothing. Because Go strings must never change, nothing may write to b again for as long as that string is alive.

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

An in-memory LRU cache budgets its entries with unsafe.Sizeof and the process is OOM-killed in production; what went wrong and how do you fix the accounting?

level: seniorimportance: should knowfreq 30%

basics

~20 s

unsafe.Sizeof charges each entry only for its fixed words, not for the bytes behind its string and slice pointers, so the budget undercounts by orders of magnitude. Charge len of the key and cap of the payload as well, then check the total against measured heap usage.

open as a page

A log tailer makes map keys with unsafe.String over bufio.Scanner.Bytes(); the counts go wrong once more lines arrive. Why?

level: seniorimportance: should knowfreq 35%

basics

~20 s

bufio.Scanner.Bytes() returns a slice into the scanner's own buffer, which the next Scan overwrites. The zero-copy string borrows those bytes, so every retained map key silently changes content while the map still files it under the old hash.

open as a page

What does unsafe.Offsetof return for a promoted field o.X versus the explicit path o.Inner.X, where Inner is embedded in o?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

unsafe.Offsetof measures from the struct the selector names. For the promoted field o.X that struct is o, so the embedded field's own offset is included. For o.Inner.X the struct is o.Inner, so the count restarts and its first field reports zero.

open as a page

Which []byte-to-string conversions does the Go compiler already do without allocating, making unsafe.String unnecessary?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

Using string(b) as a map lookup key, comparing it with ==, switching on it, or ranging over it allocates nothing: the compiler proves the temporary string cannot escape and reads the bytes in place. Reach for unsafe.String only outside those shapes.

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