skip to content

How does the Go compiler decide a struct type's alignment and total size?

level: middleimportance: should knowfreq 35%

answer

  1. one alignment wins for the whole struct
  2. the widest field sets it
  3. the total gets rounded up as well
  4. think about the second element of an array
  5. size must be a multiple of alignment

basics

~20 s

A struct's alignment is the largest alignment among its fields, and its size is rounded up to a multiple of that. The rounding adds tail padding so every element of an array of that struct stays aligned.

solid answer

~50 s

Fields are placed in declaration order: each one is pushed forward to the next offset divisible by its own alignment, and the skipped bytes are padding. The struct's alignment, reported by `unsafe.Alignof`, is the maximum alignment of its fields — 8 on a 64-bit build as soon as any field is a pointer, an `int64` or a `float64`, and 1 for a struct of only bytes and bools. The size, reported by `unsafe.Sizeof`, is the offset just past the last field rounded up to a multiple of that alignment. The tail padding is what makes arrays and slices work: element *i* lives at `i * Sizeof`, so the size must be a multiple of the alignment or every second element would be misaligned. `unsafe.Offsetof` lets you check any individual field's position, and all three are compile-time constants.

code

go · 10 lines
go
type header struct {
	version  uint8  // offset 0
	flags    uint32 // offset 4
	sequence uint64 // offset 8
	kind     uint8  // offset 16
}

// unsafe.Alignof(header{})          == 8
// unsafe.Sizeof(header{})           == 24
// unsafe.Offsetof(header{}.sequence) == 8

go deeper

for a junior

Be ready to name the two things the compiler decides for a struct — where each field starts and how big the whole value is — and that unsafe.Sizeof reports the second.

for a middle

Expect to run the layout algorithm out loud on a four-field example, produce every offset, and justify the tail padding by pointing at arrays.

for a senior

Show that you treat size as target-dependent: assert it in a test rather than quoting a number you measured once on amd64.

for a principal

Own the portability position. Deciding whether the codebase may depend on measured sizes at all, and where that is asserted, is a policy call rather than a coding one.

## The three quantities For any Go type the compiler knows a size and an alignment; for a struct it also knows an offset per field. The three `unsafe` constants expose exactly these: `unsafe.Sizeof(v)`, `unsafe.Alignof(v)` and `unsafe.Offsetof(s.f)`. They evaluate at compile time to untyped constants of type `uintptr`, so you can even use them in a constant expression or an array length. ## The layout algorithm 1. Start the running offset at 0. 2. For each field, in **declaration order**, round the running offset up to the next multiple of that field's alignment. The bytes skipped are padding. 3. Place the field there and advance the running offset by the field's size. 4. When the fields are done, set the struct's alignment to the largest field alignment (1 for a struct with no fields), and round the running offset up to a multiple of it. That final rounding is tail padding, and the result is the struct's size. On a 64-bit target the base alignments are: 1 for `bool`, `int8`, `uint8`; 2 for `int16`, `uint16`; 4 for `int32`, `uint32`, `float32`; and 8 for `int64`, `uint64`, `float64`, `int`, `uint`, `uintptr` and every pointer. Eight is the ceiling in the gc toolchain — no Go type demands more, so no struct is ever aligned to 16 or 32. A fixed-width header declared in schema order shows all four steps at once: ``` type header struct { version uint8 // offset 0 flags uint32 // offset 4 — three bytes of padding at 1..3 sequence uint64 // offset 8 kind uint8 // offset 16 — seven bytes of tail padding at 17..23 } // Alignof == 8, Sizeof == 24, but the fields carry only 14 bytes. ``` Regrouped as `sequence`, `flags`, `version`, `kind` the same header is 16 bytes: `sequence` at 0, `flags` at 8, `version` at 12, `kind` at 13, and one run of tail padding to 16. ## Composite fields - **An array** has the alignment of its element and a size of `len × elemSize` — arrays are never padded between elements, because the element size is already a multiple of the element alignment. - **A nested struct** contributes its own alignment upward. Embedding a struct whose widest field is an `int64` makes the outer struct 8-aligned even if all of its own fields are bytes. - **An empty struct**, `struct{}`, has size 0 and alignment 1. It costs nothing as a map value or a channel element. ## Zero size at the end is a special case A zero-sized field is free in the middle of a struct but not at the end. If a non-empty struct's last field has size 0, the compiler adds one byte of padding, which then rounds up to the alignment. The reason is pointer safety: taking the address of that final field must not produce a pointer to the next object in the heap, which would keep an unrelated allocation alive. So `struct{ n int64; _ struct{} }` is 16 bytes while `struct{ _ struct{}; n int64 }` is 8 — the same two members, one byte of difference in principle, eight in practice. ## Targets change the numbers Size and alignment are properties of a compilation, not of the source. On a 32-bit target `int`, `uint`, `uintptr` and pointers are four bytes wide, so a struct of two pointers is 8 bytes there and 16 on amd64. The 64-bit scalar types are the interesting case: on 32-bit x86 and 32-bit ARM the alignment of an `int64` field is 4, not 8, so a 64-bit field can land on a four-byte boundary. That is precisely why the `sync/atomic` documentation carries a caveat about arranging 64-bit alignment yourself on those platforms. Never hard-code a size you measured on your laptop into logic that has to run elsewhere; compute it with `unsafe.Sizeof` or assert it in a test that runs on each target you build for. ## Why the rules are worth knowing They turn a vague feeling that structs are bigger than they look into arithmetic you can do at a whiteboard, and they tell you which lever actually moves the number: the order of the fields and the width of their types. They also mark the limits — you cannot make a struct 14 bytes when its alignment is 8, no matter how you shuffle the fields, because the size must be a multiple of 8.

  • Why must the struct's size be a multiple of its alignment rather than just the last field's end?
    Because arrays and slices index by size. Element *i* of a `[N]T` begins at `i * unsafe.Sizeof(T{})`, so if the size were not a multiple of the alignment, the second element and every odd one after it would start on an illegal boundary. Rounding the size up guarantees every element of every array is aligned.
  • What happens to the size when a struct's last field is a zero-sized type?
    The compiler adds a byte of padding, which then rounds up to the struct's alignment. Otherwise the address of that final field would point one past the allocation and could pin the next heap object. So `struct{ n int64; _ struct{} }` is 16 bytes, while moving the empty field to the front keeps it at 8.
  • Does embedding a struct change the outer struct's alignment?
    Yes. A nested or embedded struct contributes its own alignment to the maximum, so embedding something whose widest field is an `int64` makes the outer type 8-aligned even if every field the outer type declares itself is a byte. Its fields are also laid out as one contiguous block at the embedded field's offset, not spliced in among the others.

saying these in an interview costs you the question

  • Says a struct's size is just the last field's offset plus its size
  • Thinks alignment is chosen per field only, never for the whole struct
  • Claims arrays insert padding between their elements
  • Assumes an int64 field is 8-byte aligned on every platform
  • Treats unsafe.Sizeof as a runtime call rather than a compile-time constant