| name | go-perf-struct-layout |
| description | Guides Go struct layout for performance — field alignment and padding, how field ORDER changes its size, ordering fields largest-to-smallest to shrink it (measured unsafe.Sizeof deltas), the fieldalignment analyzer and -fix, unsafe.Sizeof/Alignof/Offsetof, and why pointer-free structs scan faster under GC. Fires on "shrink this struct", "struct padding", "fieldalignment", "why is this struct so big", "align struct fields". Routes GC-scan to go-perf-allocator-internals, false sharing to go-perf-false-sharing. |
| allowed-tools | Read, Glob, Grep |
| compatibility | Claude Code, Codex CLI, Gemini CLI |
Go Struct Layout
"The compiler adds padding … to bring its size up to a multiple of the largest element in the structure … if you are looking to optimise the memory usage of your program, you need to be looking closely at the definitions of all your data structures."
— Dave Cheney, Padding is hard
Struct correctness and idiom (field naming, embedding, tags, zero-value design) belong to go-naming-and-style and go-zero-values-and-construction — read those for "how should this struct look." This skill owns one narrow performance fact: the order you write fields changes how many bytes the struct occupies and how much the GC must scan, and you can shrink a hot struct by reordering alone — same fields, same behavior, fewer bytes.
Whether you should reorder is a go-perf-methodology question (§5); the numbers below are real, measured under go1.26.0.
All Go below builds clean under gofmt, go vet, and go test on Go 1.26.
1. The Alignment Rules (the whole model)
Two guarantees from the language spec produce every padding byte you will ever see.
Sizes are fixed. For the numeric types the spec guarantees: byte, uint8, int8 = 1; uint16, int16 = 2; uint32, int32, float32 = 4; uint64, int64, float64, complex64 = 8; complex128 = 16 (Go spec, Size and alignment guarantees). On a 64-bit target a pointer, int, uintptr, map, and chan are 8; a string is 16 (ptr+len); a slice is 24 (ptr+len+cap).
Alignment is the rule that forces padding. The spec's minimal guarantees (Go spec):
- For a variable
x of any type: unsafe.Alignof(x) is at least 1.
- For a variable
x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
- For a variable
x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.
From these, two consequences drive everything:
- A field of alignment
a must start at an offset that is a multiple of a. A int64 (align 8) cannot start at offset 1; the compiler inserts padding bytes before it. Measured: Alignof(bool)=1, Alignof(int32)=4, Alignof(int64)=8, Alignof(float64)=8, pointer align =8.
- The struct's size rounds up to a multiple of the struct's own alignment (rule 2: the max field alignment) — so an array of the struct keeps every element aligned. That is the trailing padding (Padding is hard).
One layout fact has a correctness edge: on 32-bit platforms a 64-bit field accessed with sync/atomic must be 64-bit aligned, and the spec only guarantees alignment for "the first word in … an allocated struct, array, or slice" — so a misordered int64/uint64 atomic field can panic on 32-bit. Put such fields first, or use the typed atomic.Int64 (which carries its own alignment). The correctness rule is go-sync-primitives' / go-perf-atomics-vs-locks' to own; layout is just the lever.
2. Field Order Changes the Size — Measured
Same fields, two orders. Three bools interleaved with two int64s:
type Bad struct {
a bool
b int64
c bool
d int64
e bool
}
type Good struct {
b int64
d int64
a bool
c bool
e bool
}
Measured with unsafe.Sizeof under go1.26.0:
| Struct | unsafe.Sizeof | Wasted padding | Field offsets |
|---|
Bad | 40 | 21 bytes | a=0, b=8, c=16, d=24, e=32 |
Good | 24 | 5 bytes | b=0, d=8, a=16, c=17, e=18 |
The same layout drawn byte-by-byte makes the holes literal (. = padding):
Bad (40): a . . . . . . . | b b b b b b b b | c . . . . . . . | d d d d d d d d | e . . . . . . .
0 8 16 24 32
Good (24): b b b b b b b b | d d d d d d d d | a c e . . . . .
0 8 16
A 40% reduction (40 → 24) from reordering alone — no field added or removed. A second, smaller case: struct{ a bool; b int32; c bool } measures 12 bytes, but struct{ b int32; a bool; c bool } measures 8 — a 33% cut. The rule of thumb: order fields from largest alignment to smallest and the holes collapse. (Exception: when a field must be cache-line isolated to avoid false sharing → §6.)
3. The fieldalignment Analyzer (and -fix)
Don't compute padding by hand — the analyzer does it and rewrites the struct. It "finds structs that would use less memory if their fields were sorted … and provides a suggested edit with the most compact order" (fieldalignment). It ships outside vet/gopls because its findings "very rarely indicate a significant problem," so install and run it standalone (fieldalignment):
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
fieldalignment ./...
fieldalignment -fix ./...
Run against the Bad struct above, the real output was:
s.go:3:10: struct of size 40 could be 24
and -fix rewrote it to b int64; d int64; a bool; c bool; e bool — exactly largest-first, matching Good at 24 bytes. Always re-run your tests after -fix: reordering is safe for in-memory size but changes binary field offsets, so it can break code that depends on layout (unsafe, cgo, encoding/binary on the struct, reflection that assumes an order).
4. unsafe.Sizeof / Alignof / Offsetof — Prove It, Don't Guess
Never assert a struct's size from memory; measure it. These are compile-time constants (for fixed-size types), so they cost nothing at runtime (unsafe):
unsafe.Sizeof(x) — "the size in bytes of a hypothetical variable v … For a struct, the size includes any padding introduced by field alignment." It does not include memory referenced through the value — Sizeof of a slice is the 24-byte descriptor, not the backing array (unsafe).
unsafe.Alignof(x) — "the required alignment" (rule 2 above); for a struct field s.f it returns the field's in-struct alignment (unsafe).
unsafe.Offsetof(s.f) — "the number of bytes between the start of the struct and the start of the field" — the direct way to see the padding holes (unsafe).
fmt.Println(unsafe.Sizeof(Bad{}))
fmt.Println(unsafe.Offsetof(Bad{}.b))
fmt.Println(unsafe.Sizeof([]byte(nil)))
A growing offset gap between adjacent fields is the padding; that is the cheapest diagnostic.
5. When Smaller Structs Actually Pay (and when not)
Shrinking a struct helps three ways, all proportional to how hot and numerous it is:
- Cache footprint. A 64-byte cache line holds one 40-byte struct but two 24-byte ones. Iterating a
[]T of the smaller struct touches roughly half the cache lines, so the prefetcher and L1/L2 do less work — the win shows up on large slices, not on a single value.
- Fewer allocator bytes. A smaller struct may drop into a smaller size class, so each heap allocation costs less and the heap grows slower (size-class mechanics →
go-perf-allocator-internals).
- Less GC scan work when the shrink also removes pointers (§6).
The tradeoff, explicitly: reordering trades the readable grouping of fields (logical clusters, related fields together) for compactness. For a struct that is rare, short-lived, or not on a hot path, the padding is irrelevant and you should keep the order that reads best — go-perf-methodology's "is this worth optimizing?" gate applies. Reorder for size only when a profile or an allocation count says this struct's footprint matters; otherwise the clarity loss is unpaid-for. Don't sprinkle fieldalignment -fix across a whole codebase.
6. Pointer-Free Structs Scan Faster Under GC
Beyond raw size, the GC has to scan a struct for pointers — and field layout decides how much. fieldalignment reports a second diagnostic, "pointer bytes": "how many bytes of the object the garbage collector has to potentially scan for pointers." Its own examples: struct { uint32; string } "have 16 pointer bytes because the garbage collector has to scan up through the string's inner pointer," while struct { string; uint32 } "has 8 because it can stop immediately after the string pointer" (fieldalignment). (Both measure 24 bytes via unsafe.Sizeof — same size, different scan cost.)
The strongest version: a struct with no pointer fields at all (only fixed-width numerics/bools/arrays of such) is allocated in a no-scan span the GC never walks. The fastest hot struct is therefore both small and pointer-free — e.g. store an index uint32 into a slab instead of a *T. The allocator-side mechanics (scan vs no-scan spans, the bitmap) are go-perf-allocator-internals; this skill's rule is just: if a struct can be pointer-free, the GC stops touching it.
7. Don't
- Don't guess a struct's size — measure with
unsafe.Sizeof; it includes padding (unsafe).
- Don't leave a hot, numerous struct in declaration order when
fieldalignment says it "could be" smaller — interleaved small/large fields waste bytes (§2).
- Don't reorder a rare or cold struct for size at the cost of readability — the padding is free there; let methodology gate it (§5).
- Don't run
fieldalignment -fix and skip the tests — it changes field offsets, which breaks unsafe/cgo/encoding/binary/reflection that assumes a layout (§3).
- Don't assume reordering changes GC cost — it changes size; only removing pointers removes scan work (§6 →
go-perf-allocator-internals).
- Don't blindly pack the most compact order on a concurrently-written struct — adjacent hot fields can land on one cache line and false-share; that needs separation padding, the opposite move (§ →
go-perf-false-sharing).
- Don't
Sizeof a slice/map/string and think you measured the data — you measured the header (unsafe).
8. Routing to Related Skills
This plugin (go-perf-* depth):
go-perf-allocator-internals — size classes, the tiny allocator, and scan-vs-no-scan spans (why pointer-free structs skip GC scanning).
go-perf-false-sharing — padding to separate concurrently-written fields onto distinct cache lines (cpu.CacheLinePad) — the inverse of compacting.
go-perf-data-oriented-layout — struct-of-arrays vs array-of-structs, intrusive/ring structures, cache-friendly access patterns.
go-perf-methodology — the "is shrinking this struct worth it?" decision and the measure-first gate.
Marketplace (go-* correctness):
go-naming-and-style — struct field naming, embedding, tags, idiomatic shape.
go-zero-values-and-construction — useful zero values and constructor design (layout serves these, never overrides them).
go-slices-and-maps — slice/map header semantics behind the Sizeof-header note.
9. Reference Files
Wrong/right struct-layout mistakes in LLM-generated Go, each with a citation:
${CLAUDE_SKILL_DIR}/references/common-mistakes.md
Source provenance for every claim in this skill:
${CLAUDE_SKILL_DIR}/references/sources.yaml