skip to content

Struct Alignment and Padding

Fields sit in declaration order with padding inserted to meet each type's alignment, so reordering fields changes the struct's size. Interviewers use it to see if you can predict unsafe.Sizeof.

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

questions

4

Why is a Go struct sometimes larger than the sum of its field sizes?

level: juniorimportance: should knowfreq 45%

answer

  1. the compiler is not packing them tightly
  2. each type wants a legal start address
  3. holes open between neighbours of different widths
  4. declaration order decides where holes fall
  5. widest fields first, bools last

basics

~20 s

Go inserts padding bytes so each field starts at an address its type requires, such as an int64 on an 8-byte boundary. Padding follows declaration order, so the same fields in a different order can be smaller.

solid answer

~50 s

Every Go type has an alignment. On a 64-bit build an `int64`, a `float64` or a pointer must start at a multiple of 8, an `int32` at a multiple of 4, and a `bool` anywhere. The compiler lays the fields out in the order you wrote them and inserts padding bytes so each one lands on a legal boundary, then pads the end so the whole struct is a multiple of its own alignment. That is why `struct{ a bool; n int64; b bool }` is 24 bytes on amd64 — one byte, seven bytes of padding, eight bytes, one byte, then six bytes of tail padding — while `struct{ n int64; a, b bool }` holding exactly the same data is 16. `unsafe.Sizeof` reports the padded size, and grouping the widest fields first is the usual way to shrink it.

code

go · 14 lines
go
type wide struct {
	a bool
	n int64
	b bool
}

type narrow struct {
	n int64
	a bool
	b bool
}

// unsafe.Sizeof(wide{})   == 24
// unsafe.Sizeof(narrow{}) == 16

go deeper

for a junior

Be ready to say why the total exceeds the field sizes, and to point at the specific alignment boundary that forced each gap in a small example.

for a middle

Expect to compute the offsets by hand for a three-field struct and to explain why the end of the struct is padded as well as the middle.

for a senior

Show judgment about when shrinking a struct is worth doing at all: millions of instances in one slice, yes; a config value created once at startup, no.

for a principal

Own whether field ordering for size becomes a house convention. It trades readable, schema-shaped declarations for memory that somebody has to be able to measure and defend.

## Two numbers per type The compiler attaches two numbers to every Go type: a **size** in bytes and an **alignment** in bytes. Alignment is a hardware-derived requirement — a value of that type must begin at an address that is a multiple of its alignment. In the gc toolchain on a 64-bit target the alignments are: | type | size | alignment | |---|---|---| | `bool`, `int8`, `uint8` | 1 | 1 | | `int16`, `uint16` | 2 | 2 | | `int32`, `uint32`, `float32`, `rune` | 4 | 4 | | `int64`, `uint64`, `float64`, `int`, `uint`, `uintptr`, any pointer | 8 | 8 | An array takes the alignment of its element type. A struct takes the **largest** alignment of any field it contains. ## How the fields are placed The compiler walks the fields **in the order you declared them**. The first field goes at offset 0. For each field after that it advances the running offset to the next multiple of that field's alignment — the bytes skipped over are **padding** — and places the field there. When every field has a home, the total is rounded up to a multiple of the struct's own alignment, and those extra bytes are **tail padding**. Work the classic example by hand on a 64-bit build: ``` type flags struct { a bool // offset 0, size 1 // offsets 1..7 are padding n int64 // offset 8, size 8 b bool // offset 16, size 1 // offsets 17..23 are tail padding } ``` `a` sits at 0. `n` needs an offset divisible by 8, so seven bytes are skipped and it lands at 8. `b` follows at 16. The struct's alignment is 8 (its widest field), so the size rounds up from 17 to 24. Ten bytes of data occupy 24 bytes of memory. Move `n` to the front and nothing is wasted in the middle: ``` type flags struct { n int64 // offset 0 a bool // offset 8 b bool // offset 9 // offsets 10..15 are tail padding } ``` Size 16. Same fields, same types, same meaning — eight bytes smaller. ## Why the end is padded too Tail padding exists so that arrays work. In a `[3]flags`, element *i* starts at `i * unsafe.Sizeof(flags{})`. If the size were 17, the second element would start at offset 17 and its `int64` would be misaligned. Rounding the size up to a multiple of the alignment guarantees that every element of every array — and every element of a slice, which is backed by one — starts on a legal boundary. ## Choosing an order The practical rule is to declare fields in **descending order of alignment**: pointers and 8-byte scalars first, then 4-byte, then 2-byte, then the `bool`s and `byte`s last. That leaves at most one run of padding, at the tail. It is not provably optimal in every exotic case, but it gets you to the minimum for almost every real struct. The compiler will not do this for you. The gc toolchain lays fields out exactly as declared, so the ordering is part of your source, reviewed like any other line. ## What padding is not - It is **not** part of comparison. Struct equality is defined field by field, so two structs with equal fields are equal regardless of what sits in the gaps. - It is **not** something you can switch off. Go has no packing pragma; reordering is the only lever, alongside choosing narrower field types. - It is **not** the encoding. `encoding/json` and `encoding/binary` write fields, not memory, so padding never appears on the wire. Note that both of them emit fields in declaration order, so reordering does change the byte order that `encoding/binary` produces and the key order in JSON output. - It is **not** always worth chasing. Eight wasted bytes in a struct you allocate once is noise; eight wasted bytes in a slice of ten million is 80 MB. ## Measuring it `unsafe.Sizeof(v)` gives the real in-memory size including padding, `unsafe.Alignof(v)` gives the alignment, and `unsafe.Offsetof(s.f)` gives a field's offset within its struct. All three are ordinary compile-time constants — using them to inspect a layout involves none of the risk normally associated with the `unsafe` package. Sizes differ between targets: on a 32-bit build `int` and pointers are four bytes wide, so the same declaration can produce a different size.

  • What order would you declare the fields in to make a struct as small as possible?
    Descending alignment: pointers and 8-byte scalars such as `int64`, `float64` and `int` first, then 4-byte fields, then 2-byte, then `bool` and `byte` last. That leaves at most one run of padding at the tail. It is a heuristic rather than a proof, but it reaches the minimum for essentially every struct you will meet.
  • Does reordering fields change anything a caller can observe?
    Not the semantics: field names, types, values and struct equality are unchanged, and equality is defined field by field so padding never participates. Two things do change. `encoding/binary` writes fields in declaration order, so the bytes it produces move. And `encoding/json` emits keys in declaration order, so the output text changes even though any decoder reads it identically.
  • Is the padding guaranteed to be zeroed?
    Do not rely on it. A freshly zeroed struct value has zeroed padding, but copying a struct copies whole words, so gaps can carry over whatever was in the source. Nothing in Go reads those bytes for you — comparison is field-wise and encoders write fields — so the only way to observe padding content is to reinterpret the memory yourself.

Think of parking bays where a lorry may only start on every eighth bay. Park a bicycle in the first bay and the lorry still has to start at bay eight, leaving seven bays empty behind it.

saying these in an interview costs you the question

  • Says Go packs struct fields with no gaps between them
  • Claims the Go compiler reorders fields to shrink the struct
  • Thinks unsafe.Sizeof returns the sum of the field sizes
  • Assumes padding appears only between fields, never at the end
  • Believes reordering fields changes the struct's meaning or its JSON field names
open as a page

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

level: middleimportance: should knowfreq 35%

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.

open as a page

How would you prove that struct padding, not live data, is inflating a Go service's heap?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Measure rather than guess. Compare unsafe.Sizeof of the struct against the sum of its field sizes, multiply the difference by the live instance count from a heap profile, and check that the product accounts for the missing memory.

open as a page

Does the Go compiler reorder struct fields to eliminate padding?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

No. The gc toolchain lays fields out in declaration order, so removing padding is the programmer's job. Rust's default representation may reorder fields, which is one reason engineers arriving from it expect Go to compact a struct for them.

open as a page