unsafe.Sizeof Alignof and Offsetof

unsafe.Sizeof, unsafe.Alignof, and unsafe.Offsetof are the three layout-query functions of the unsafe package. Unlike unsafe.Pointer, they do not manipulate addresses and cannot corrupt memory — they ask the compiler “how big is this type, how is it aligned, and where does this field sit?” and the compiler answers with a uintptr, usually a compile-time constant. The unsafe documentation defines each precisely: Sizeof “returns the size in bytes,” Alignof “returns the required alignment,” and Offsetof “returns the offset within the struct of the field” (pkg.go.dev/unsafe). They are the bridge between the abstract type system and the physical bytes — the tools you reach for to write architecture-aware code, validate struct packing, or compute the offsets that feed unsafe.Pointer arithmetic.

Mental Model

Think of these three as the compiler’s layout calculator, exposed to the program. Every Go type has a physical realization — a number of bytes, an alignment requirement, and (for structs) a position for each field. The compiler computes all of this anyway during code generation; Sizeof/Alignof/Offsetof simply hand those numbers back to you.

flowchart TD
    T["A Go type / value / field"]
    T --> S["Sizeof(x)<br/>bytes the value occupies"]
    T --> A["Alignof(x)<br/>address must be 0 mod m"]
    T --> O["Offsetof(s.f)<br/>bytes from struct start to field"]
    S & A & O --> C{"Variable size?<br/>(type param, or array/struct<br/>of variable-size elements)"}
    C -->|No| K["uintptr CONSTANT<br/>folded at compile time"]
    C -->|Yes| R["uintptr value computed at run time"]
    style K fill:#e8f5e9
    style R fill:#fff3e0

Diagram: each function maps a type/value/field to a uintptr. The branch at the bottom is the subtle part: the result is a compile-time constant (green) for ordinary types, but a runtime value (orange) when the type has variable size — i.e. it is a type parameter, or an array/struct whose elements have variable size. The insight: in non-generic code these calls cost nothing at runtime; inside generic code parameterized over T, unsafe.Sizeof(t) becomes a genuine runtime read.

The key idea is that these functions describe storage, not content. Sizeof of a slice is the size of the three-word slice header, not the backing array; Sizeof of an interface is the two-word interface value, not the boxed concrete value. They measure the box, never what is in it.

Mechanical Walk-through

unsafe.Sizeof

The documentation: “Sizeof takes an expression x of any type and returns the size in bytes of a hypothetical variable v as if v was declared via var v = x. The size does not include any memory possibly referenced by x.” Three consequences:

  • Headers, not payloads. “if x is a slice, Sizeof returns the size of the slice descriptor, not the size of the memory referenced by the slice; if x is an interface, Sizeof returns the size of the interface value itself.” A []byte of a million bytes has Sizeof 24 on a 64-bit platform (pointer + len + cap, each one word — see Slice Internals).
  • Padding is included. “For a struct, the size includes any padding introduced by field alignment.” Sizeof of a struct is the size the compiler actually reserves, trailing pad bytes and all.
  • Constant when fixed-size. “The return value of Sizeof is a Go constant if the type of the argument x does not have variable size. (A type has variable size if it is a type parameter or if it is an array or struct type with elements of variable size).”

The argument is unevaluatedunsafe.Sizeof(x) does not run x; it only inspects its static type. Writing unsafe.Sizeof(someFunc()) does not call someFunc.

unsafe.Alignof

The documentation: “Alignof takes an expression x of any type and returns the required alignment of a hypothetical variable v … It is the largest value m such that the address of v is always zero mod m.” Walking the formula symbol by symbol: m is the alignment; “the address of v is always zero mod m” means the variable’s address, divided by m, leaves remainder zero — i.e. the address is a multiple of m. A larger m is a stricter requirement.

The docs add that Alignof(x) equals reflect.TypeOf(x).Align(), and define a special case for struct fields: “if a variable s is of struct type and f is a field within that struct, then Alignof(s.f) will return the required alignment of a field of that type within a struct … the same as the value returned by reflect.TypeOf(s.f).FieldAlign().” On most platforms the field alignment and the type alignment coincide, but they can differ (notably for 64-bit scalars on 32-bit architectures, where the field alignment inside a struct may be looser than the type’s natural alignment — the cause of the well-known 32-bit sync/atomic alignment gotcha).

unsafe.Offsetof

The documentation: “Offsetof returns the offset within the struct of the field represented by x, which must be of the form structValue.field. In other words, it returns the number of bytes between the start of the struct and the start of the field.” The argument syntax is constrained: it must be a field selector, e.g. unsafe.Offsetof(s.f) or unsafe.Offsetof(s.embedded.g) for a promoted field. The result is constant under the same “no variable size” rule as Sizeof.

Why alignment exists at all

Before walking the algorithm, it is worth being concrete about why a type has an alignment requirement, because Alignof is otherwise an arbitrary-looking number. CPUs read memory in word-sized chunks, and most architectures either fault, or silently take a multi-cycle penalty, when a multi-byte value straddles a natural boundary — e.g. when a 4-byte int32 spans the gap between two 4-byte-aligned words. To avoid that, the compiler guarantees each value sits at an address that is a multiple of its size class. A bool or int8 may live at any address (alignment 1); an int16 at an even address (alignment 2); an int32/float32 at a multiple of 4; an int64/float64/pointer at a multiple of 8 on a 64-bit platform. A struct’s alignment is the maximum of its fields’ alignments, because the strictest field must still land on a legal boundary no matter where the struct itself is placed. Alignof simply reports the number the compiler is already enforcing — and “the address of v is always zero mod m” is just the formal statement of “lands on a legal boundary.”

This is also the root of false sharing: two independently-mutated variables that share a 64-byte cache line ping-pong that line between CPU cores even though neither touches the other’s bytes. Code that cares uses Sizeof/Alignof to pad hot structs out to a full cache line — see Memory Alignment and False Sharing.

How layout is actually computed

These numbers are not magic — they follow Go’s struct-layout algorithm, which the compiler runs in cmd/compile/internal/types. Fields are laid out in declaration order — Go, unlike some C compilers under certain flags, never reorders struct fields, so a poorly ordered struct stays poorly ordered and Sizeof reflects it. Before placing each field, the compiler advances the running offset up to a multiple of that field’s alignment, inserting padding bytes if needed. After the last field, the whole struct’s size is rounded up to a multiple of the struct’s own alignment (the maximum of its fields’ alignments) so that arrays of the struct keep every element aligned. This is why field order changes Sizeof:

type Bad struct {
	a bool   // offset 0, 1 byte
	_        // 7 bytes padding (b needs 8-alignment)
	b int64  // offset 8
	c bool   // offset 16, 1 byte
	_        // 7 bytes trailing padding (struct rounds to 8)
}            // Sizeof(Bad{}) == 24
 
type Good struct {
	b int64  // offset 0
	a bool   // offset 8
	c bool   // offset 9
	_        // 6 bytes trailing padding
}            // Sizeof(Good{}) == 16

Same three fields, 24 versus 16 bytes — the topic of Struct Memory Layout and Alignment.

Code — Line-by-Line

package main
 
import (
	"fmt"
	"unsafe"
)
 
type Packet struct {
	Flags   uint8   // offset 0
	_       [3]byte // explicit padding to align Length
	Length  uint32  // offset 4
	Payload []byte  // offset 8  (slice header, 3 words)
}
 
func main() {
	var p Packet
	fmt.Println(unsafe.Sizeof(p))            // 32 on a 64-bit platform
	fmt.Println(unsafe.Alignof(p))           // 8 — driven by the slice's pointer word
	fmt.Println(unsafe.Offsetof(p.Length))   // 4
	fmt.Println(unsafe.Offsetof(p.Payload))  // 8
	fmt.Println(unsafe.Sizeof(p.Payload))    // 24 — the header, NOT the bytes
 
	const headerBytes = unsafe.Offsetof(Packet{}.Payload) // a compile-time const: 8
	var buf [headerBytes]byte                              // legal: const array size
	_ = buf
}
  1. unsafe.Sizeof(p) — 32: Flags(1) + padding(3) + Length(4) + Payload header(24) = 32; the struct is already 8-aligned so no trailing pad.
  2. unsafe.Alignof(p) — 8: the strictest field is the slice header, whose first word is a pointer (8-byte alignment on a 64-bit platform).
  3. unsafe.Offsetof(p.Length) — 4: after the 1-byte flag and 3 explicit pad bytes.
  4. unsafe.Sizeof(p.Payload) — 24: the slice descriptor, three machine words; the actual []byte contents are not counted.
  5. const headerBytes = unsafe.Offsetof(...) — because Packet has no variable-size element, the result is a Go constant, usable as an array length on the next line. That a var buf [...]byte compiles is the proof.

A second, idiomatic use — feeding unsafe.Pointer arithmetic (conversion pattern 3 of unsafe.Pointer Conversion Rules):

// Address of p.Length without writing &p.Length, by manual offset arithmetic.
lp := (*uint32)(unsafe.Pointer(uintptr(unsafe.Pointer(&p)) + unsafe.Offsetof(p.Length)))
*lp = 1500

unsafe.Offsetof(p.Length) supplies the byte offset; the surrounding cast is exactly the documented “equivalent to f := unsafe.Pointer(&s.f)” idiom. In modern code unsafe.Add(unsafe.Pointer(&p), unsafe.Offsetof(p.Length)) reads more cleanly (see unsafe.Slice and unsafe.String for the Add family).

A third use is the struct-size regression guard, an everyday production pattern that costs nothing at runtime:

// Fails to compile if Packet ever grows past 32 bytes — e.g. a teammate
// adds a field. The whole assertion is constant-folded; the const block
// produces no code and no runtime check.
const _ = unsafe.Sizeof(Packet{}) // forces evaluation
const packetSizeGuard = 1 / (32 / int(unsafe.Sizeof(Packet{})))

The trick: int(unsafe.Sizeof(Packet{})) is a compile-time constant. If the struct is exactly 32 bytes, 32/32 == 1 and 1/1 == 1 — fine. If it grows to 40 bytes, 32/40 == 0 (integer division), and 1/0 is a compile-time division-by-zero error. A more readable equivalent with newer Go is a plain const boolean fed to a […]struct{} of negative length, or simply a unit test calling t.Fatalf when unsafe.Sizeof diverges from an expected value. Either way the point stands: because the result is a constant, layout assertions can be enforced by the build itself.

The fourth common use is computing element strides for unsafe.Slice/unsafe.Add over a typed array obtained from C or mmapunsafe.Sizeof(elem) is exactly the stride, and the compiler folds it. See unsafe.Slice and unsafe.String.

Failure Modes and Common Misunderstandings

Expecting Sizeof to count referenced memory. unsafe.Sizeof of a string, []T, map, chan, or *T returns the header/pointer size, never the pointee. To size the contents you must walk them yourself or use reflect. This trips up anyone trying to estimate a value’s true memory footprint.

Hard-coding the result. unsafe.Sizeof(int(0)) is 8 on a 64-bit build and 4 on a 32-bit build; pointer-containing struct sizes shift the same way. A constant copied from one architecture silently miscomputes on another — always call the function, never bake in the number (see Word Size and Architecture Portability).

The 32-bit atomic-alignment gotcha. On 32-bit architectures a 64-bit field’s alignment within a struct may be 4, not 8, even though 64-bit atomic operations require 8-byte alignment. Alignof(s.f) (the FieldAlign value) reveals this; ignoring it causes sync/atomic 64-bit operations to panic on 32-bit platforms. The sync/atomic package documents the exact rule in its Bugs section: “On ARM, 386, and 32-bit MIPS, it is the caller’s responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically via the primitive atomic functions … The first word in an allocated struct, array, or slice; in a global variable; or in a local variable … can be relied upon to be 64-bit aligned” (pkg.go.dev/sync/atomic). So the classic fix is to put the 64-bit field first in the struct, forcing the favorable position. The modern fix is simpler: the same Bugs note records that the struct-based atomic types added in Go 1.19 — atomic.Int64 and atomic.Uint64 — “are automatically aligned” regardless of position, because they carry an internal alignment-forcing field. New code on 32-bit targets should prefer those typed wrappers over the bare atomic.LoadInt64/StoreInt64 functions precisely to sidestep this trap.

Forgetting Offsetof needs a selector. unsafe.Offsetof(p) (no field) is a compile error; the argument must be structValue.field. For a promoted/embedded field, unsafe.Offsetof(s.embedded.field) works and yields the offset from the outer struct.

Assuming the constant property survives generics. Inside a function parameterized over T, unsafe.Sizeof(t) is not a constant — T is a type parameter, hence “variable size.” It compiles, but the result is a runtime value and cannot be used where a constant is required.

Alternatives and When to Choose Them

  • reflect. reflect.TypeOf(x).Size(), .Align(), .FieldAlign(), and StructField.Offset give the same numbers at runtime, for dynamically discovered types (where the static type is not known at the call site). The unsafe functions are constant-folded and need no reflect import; reflect is the right choice only when the type is not statically known. The docs explicitly tie the two together (Alignof(x) == reflect.TypeOf(x).Align()).
  • reflect/StructField.Offset — when iterating fields generically, this is cleaner than building Offsetof calls.
  • Manual byte-counting / encoding/binary. When the goal is portable serialization rather than introspection, define the wire format explicitly and use encoding/binary — do not let Sizeof of a Go struct define your on-disk or on-wire format, because padding and word size are not portable.

Use unsafe.Sizeof/Alignof/Offsetof when the type is statically known and you want a zero-cost compile-time constant; use reflect when the type is discovered at runtime.

Production Notes

These three functions are the safe corner of unsafe: they never produce a pointer and never risk a use-after-free, so importing unsafe solely for them is uncontroversial. Real-world uses include: assertion tests that pin a struct’s Sizeof to a known value so an accidental field addition that bloats a hot struct fails CI; cache-line-aware code that checks Sizeof against 64 bytes to control false sharing; and the offset constants that high-performance serializers and the reflect package itself rely on. The slice-descriptor figure of 24 bytes used throughout this note is the direct consequence of the runtime’s three-word slice layout — type slice struct { array unsafe.Pointer; len int; cap int }, i.e. one 8-byte pointer plus two 8-byte ints on a 64-bit platform (runtime/slice.go). The Go blog’s “storage size and alignment” material and Go’s own spec text on unsafe are the authoritative references; the layout algorithm those numbers obey lives in cmd/compile/internal/types/size.go.

Uncertain

Verify: the exact field alignment value of a 64-bit scalar inside a struct on each individual 32-bit GOARCH (386, arm, mips, mipsle). Reason: the language spec only guarantees minimum alignments, and sync/atomic documents that on these arches a 64-bit word’s in-struct alignment can be 4 rather than 8 — but the precise per-architecture ABI value was not re-checked field-by-field against a primary per-arch table for Go 1.26. The headline claim (that the gotcha exists and how to avoid it) is verified against the sync/atomic Bugs note; only the exact numeric alignment on each minor 32-bit arch is unconfirmed. To resolve: read the per-architecture MAXWIDTH/alignment constants in cmd/compile/internal/<arch> at the Go 1.26 tag. uncertain

See Also