skip to content

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%

answer

  1. measured relative to something
  2. which struct does the selector name?
  3. promotion sums the offsets along the path
  4. an explicit path restarts the count at zero

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.

solid answer

~40 s

`unsafe.Offsetof` takes a selector `s.f` and returns the byte offset of `f` relative to the address of the struct that `s` denotes. With a promoted field the selector base is the outer struct, so `unsafe.Offsetof(o.X)` adds the offset of the embedded field inside `o` to the offset of `X` inside it. Write the path explicitly and the base changes: in `o.Inner.X` the struct is `o.Inner`, so you get X's offset within `Inner` — typically 0 for its first field. Both forms are compile-time `uintptr` constants, so if you need the outer offset from an explicit path you can simply add `unsafe.Offsetof(o.Inner)` to it. One restriction: a promoted field reached through an *embedded pointer* is rejected at compile time, because reaching it requires an indirection and no constant offset can express that.

code

go · 14 lines
go
type Inner struct {
	X int64
}

type Outer struct {
	P int64
	Inner // embedded by value
}

var o Outer

const a = unsafe.Offsetof(o.X)       // 8: promoted, measured from Outer
const b = unsafe.Offsetof(o.Inner.X) // 0: measured from Inner
const c = unsafe.Offsetof(o.Inner)   // 8: the embedded field itself

go deeper

for a junior

Know that Offsetof answers where a field starts inside its struct, in bytes, and that you hand it something like variable.field rather than a type name.

for a middle

Be able to state the rule that the offset is relative to the struct the selector names, and to work out both answers for an embedded struct without running the code.

for a senior

Spot the wrong-base bug in review: an offset computed from an inner struct and then applied to an outer address compiles cleanly and misreads memory. Know that an embedded pointer makes the whole approach impossible.

for a principal

Set the expectation for code that encodes layout: every offset should be pinned by a compile-time assertion or generated, never hand-copied, because a field inserted by someone else will not announce itself.

## The rule The specification defines `unsafe.Offsetof` precisely: it takes a possibly parenthesised selector `s.f`, denoting a field `f` of the struct denoted by `s` or `*s`, and returns the field offset in bytes relative to that struct's address. Two things in that sentence do the work — **the argument must be a selector**, and **the offset is relative to the struct the selector's base names**, not to some outermost struct the compiler might guess at. Embedding is where those two facts bite, because Go lets you reach the same field by two different selectors. ```go type Inner struct { X int64 } type Outer struct { P int64 Inner // embedded by value } var o Outer ``` - `unsafe.Offsetof(o.X)` — `X` is a *promoted* field: the selector base is `o`, so the offset is measured from the start of `Outer`. The compiler walks the promotion path and sums the offsets: 8 for `Inner` inside `Outer`, plus 0 for `X` inside `Inner`, giving **8**. - `unsafe.Offsetof(o.Inner.X)` — here the selector is `(o.Inner).X`, so the struct is `o.Inner` and the offset is measured from the start of `Inner`: **0**. - `unsafe.Offsetof(o.Inner)` — the embedded field is itself a field of `Outer`, so this is **8**. The two forms name the same memory but answer different questions, and mixing them up is a real bug when the number is used to compute an address. ## Embedded pointers are rejected If the embedding is by pointer, promotion still works for ordinary field access but Offsetof does not: ```go type Outer struct { P int64 *Inner } ``` Now `o.X` compiles as an expression — Go follows the pointer for you — but `unsafe.Offsetof(o.X)` is a compile-time error. The specification requires an embedded field to be reachable without pointer indirections, and for good reason: X's address is not at a fixed distance from o's address, so no constant can describe it. What still works is `unsafe.Offsetof(o.Inner)`, the offset of the pointer field itself, since that field genuinely sits inside `Outer`. ## Other constraints worth knowing - **You need a value, not a type.** There is no `unsafe.Offsetof(Outer.P)` form. The idiom when you have no convenient variable is a package-level zero value: `var zero Outer` and then `const off = unsafe.Offsetof(zero.P)`. - **A pointer base is fine.** If `p` is an `*Outer`, `unsafe.Offsetof(p.P)` is legal — the selector automatically dereferences, and the result is still a constant, because only the type of the base is used. - **It is a constant.** Like Sizeof and Alignof, the result is a `uintptr` constant whenever the struct's type has constant size, so it can appear in a `const` declaration or an array length. The exception is a struct type built from type parameters, whose layout is not known while compiling. - **Offsets compose by addition.** Because every form is a constant, `unsafe.Offsetof(o.Inner) + unsafe.Offsetof(o.Inner.X)` is a constant equal to the promoted form's answer. That is often the clearest way to write a deep offset, since it makes the base of each measurement explicit. ## Where this shows up Offsetof is mostly used in code that lays out memory to match something outside Go — a binary record format, a structure defined by a C header, a memory-mapped region — and in assertions that pin a layout down so a future edit cannot quietly change it. In all of those uses, an offset measured from the wrong base produces a number that is plausible, compiles cleanly, and is wrong at run time, which is exactly the class of mistake worth being able to spot in review.

  • What changes if the embedded field is declared as *Inner instead of Inner?
    Field access `o.X` still works, but `unsafe.Offsetof(o.X)` becomes a compile-time error: the specification requires a promoted field to be reachable without pointer indirections, and X is not at a constant distance from o. `unsafe.Offsetof(o.Inner)` remains legal, because the pointer field itself sits inside the outer struct.
  • Can you ask for an offset without having a variable, for example unsafe.Offsetof(Outer.P)?
    No — the argument must be a selector on a value, and `Outer.P` is not a valid expression for a field. The standard workaround is a zero value: declare `var zero Outer` and write `const off = unsafe.Offsetof(zero.P)`. The variable is never read, since only its type matters.
  • How do you get the offset of X from the start of Outer when you have written the explicit path?
    Add the two measurements: `unsafe.Offsetof(o.Inner) + unsafe.Offsetof(o.Inner.X)`. Both are `uintptr` constants, so the sum is a constant too, and it equals what the promoted selector `unsafe.Offsetof(o.X)` reports.

It is a distance from the door of whichever room you named. Name the building and you get the distance from the entrance; name the room and the count starts at that room's threshold.

saying these in an interview costs you the question

  • Assumes Offsetof always counts from the outermost struct
  • Passes a type instead of a selector on a value
  • Expects a field promoted through an embedded pointer to compile
  • Thinks Offsetof reads layout at run time
  • Believes the two selector forms must give the same number