Go Struct Field Alignment: Why Your Structs Are Bigger Than You Think
Go struct field alignment padding is the reason a struct with three fields that should logically take 17 bytes actually takes 24. The Go compiler does not pack fields tightly. It aligns each field to a memory boundary based on its type, and it inserts invisible padding bytes between fields to satisfy that alignment — bytes that do nothing except sit there, unused, in every single instance of that struct you ever create. This page covers exactly how the compiler decides where padding goes, why field order changes the total size of a struct without changing a single type, and how to use unsafe.Sizeof and go vet -fieldalignment to find and fix the waste.
Covers Go 1.20+ on amd64 and arm64. The alignment rules described are part of the compiler’s memory layout decisions, not the language specification.
TL;DR
- Every type in Go has an alignment requirement — a
int64must start at an address divisible by 8, aboolcan start anywhere - The compiler inserts padding bytes between fields whenever needed to satisfy the next field’s alignment requirement
- Field order changes total struct size — the same fields in a different order can produce a smaller or larger struct, with zero changes to the types themselves
unsafe.Sizeof()shows you the real size including padding.unsafe.Alignof()shows you the alignment requirement that’s causing itgo vet -fieldalignmentautomatically flags structs that could be smaller with a different field order — most teams never run this check- This matters most at scale: a struct that wastes 7 bytes costs nothing for one instance and costs gigabytes for ten million instances in memory
Go Struct Field Alignment: How the Compiler Decides Where Padding Goes
Every type in Go has an alignment requirement. An int64 needs to start at a memory address divisible by 8. An int32 needs an address divisible by 4. A bool or byte can start anywhere — alignment 1. This is not a Go-specific quirk. It’s how CPUs read memory efficiently, and the compiler respects it without asking you.
Here’s the part that surprises most developers: the compiler enforces this even inside your structs, by inserting padding bytes wherever needed. Look at this struct.
// Go — struct fields in declaration order, with hidden padding
type Event struct {
Active bool // 1 byte
Timestamp int64 // 8 bytes — needs 8-byte alignment
Code int32 // 4 bytes
}
// You'd expect: 1 + 8 + 4 = 13 bytes
// fmt.Println(unsafe.Sizeof(Event{})) actually prints: 24
13 bytes of data. 24 bytes of struct. Where did the other 11 bytes go? Padding — inserted right after Active to push Timestamp onto an 8-byte boundary, and again after Code to round the whole struct up to a multiple of its largest field’s alignment.
Why Does Field Order Change Struct Size?
Because padding depends on what comes next, not on what a field weighs by itself. Put the small fields together and the big fields together, and the compiler needs far less filler to satisfy alignment. Scatter them, and every gap gets padded individually.
Goroutine Leak Patterns That Kill Your Service Without Warning A goroutine leak is a goroutine that was spawned and never terminated — it holds stack memory, blocks on a channel or syscall, and the Go...
How Much Memory Does Padding Actually Waste?
For one struct? Nothing you’d notice — 11 bytes is rounding error. For ten million structs sitting in a cache or a queue, that’s 110MB of pure padding. No data. No logic. Just bytes the CPU insisted on for alignment, multiplied by however many instances your service creates per second.
unsafe.Sizeof and Field Order: The Same Fields, Two Different Sizes
Reordering fields — without touching a single type — can shrink a struct by a third. Here’s the exact same Event struct, fields sorted largest-to-smallest.
// Go — same fields, reordered large-to-small, zero padding waste
type EventOptimized struct {
Timestamp int64 // 8 bytes — naturally aligned, no padding needed before it
Code int32 // 4 bytes
Active bool // 1 byte
}
// fmt.Println(unsafe.Sizeof(EventOptimized{})) prints: 16
// Same fields. Same types. 8 bytes saved per instance — just from order.
24 bytes versus 16. A 33% reduction, and the struct does exactly the same job. This is the entire trick: largest alignment requirement first, smallest last. Nothing else changes.
Worth saying clearly — this is not premature optimization. You’re not adding complexity or sacrificing readability. You’re choosing one of several equally readable field orders, and picking the one that doesn’t waste memory. There’s no tradeoff here, which is rare in performance work.
Does Field Order Affect CPU Cache Performance Too?
Yes, and it compounds. A smaller struct means more instances fit in a single 64-byte cache line. More instances per cache line means fewer cache misses when you iterate over a slice of them — which is exactly the access pattern most Go services use for request batches, event queues, and database result sets.
What Is unsafe.Alignof and When Do You Actually Need It
unsafe.Alignof(x) tells you the alignment requirement driving the padding — not the size, the requirement itself. You reach for it specifically when unsafe.Sizeof gives you a number larger than expected and you need to know which field is forcing the gap. In practice: run Alignof on each field, find the largest number, and that’s your answer for “why is this struct 24 bytes instead of 13.”
go vet -fieldalignment: The Check Almost Nobody Runs
This has shipped with Go’s tooling since 2021. Most Go developers have never heard of it, because it’s not part of the default go vet run — you have to ask for it explicitly.
# Go — running the fieldalignment analyzer (not enabled by default) go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest fieldalignment ./... # Output for the unoptimized Event struct from earlier: # event.go:5:2: struct of size 24 could be 16
One line. It found the exact waste, told you the exact fix, in a struct most reviewers would have approved without a second look.
Should You Run fieldalignment on Every Struct in Your Codebase?
No — be selective. Run it on hot-path structs: anything created in a loop, anything stored in a slice or map at scale, anything serialized millions of times a second. A config struct created once at startup wastes nothing that matters. A struct created per HTTP request, multiplied by ten thousand requests a second, is where this analysis actually pays for itself.
Golang Struct Size Bigger Than Expected: The First Three Things to Check
Check field order first — most surprises come from small types interleaved with large ones. Then check for nested structs, since their alignment requirement inherits from their largest internal field, not from the struct’s own apparent simplicity. Then check for embedded interfaces — these carry a fixed two-word overhead (type pointer plus data pointer) regardless of what’s behind them, which catches people off guard every time.
Hidden Performance Traps in Go That Mid-level Devs Keep Hitting Most Go codebases that end up slow weren't written by juniors who didn't know what they were doing — they were written by competent developers...
Memory Layout in Production: Where This Actually Matters
Struct padding is invisible right up until scale makes it visible. Three places where it consistently shows up.
High-throughput message queues. A struct representing a queued message, multiplied by millions of messages buffered during a traffic spike, turns 8 wasted bytes into real memory pressure — sometimes the difference between staying under a pod’s memory limit and getting OOMKilled.
In-memory caches. An LRU cache holding a million entries pays the padding cost a million times over. Shrink the struct by 8 bytes, and you’ve effectively bought back 8MB of cache capacity for free — no new hardware, no config change, just field order.
Slices of structs, not pointers. This is the case where padding actually matters most. A []LargeStruct stores every byte of padding contiguously for every element. A []*LargeStruct only pays the padding cost once per allocation — but now you’ve traded memory for a pointer indirection and worse cache locality on every access. Neither option is free; which one wins depends on whether you’re optimizing for memory footprint or access speed.
// Go — measuring the real-world cost across a slice events := make([]Event, 1_000_000) // 24 bytes each = 24MB total eventsOpt := make([]EventOptimized, 1_000_000) // 16 bytes each = 16MB total // 8MB difference, from field order alone, on one million records
Nobody notices 8 bytes. Everybody notices 8 megabytes. The gap between those two numbers is just a matter of how many instances your service creates — and most production Go services create a lot more than one.
Does This Matter for a Typical CRUD API?
Honestly — usually not. If your service handles a few hundred requests a minute and your structs aren’t sitting in a hot loop or a massive in-memory collection, the padding overhead is noise. This is a scale problem, not a correctness problem. Optimize structs that get created millions of times; leave the rest alone.
Struct Field Order vs JSON Field Order: Two Unrelated Concepts
Worth clearing up, because it trips people up constantly: struct field declaration order controls memory layout. JSON struct tags control serialization order. Changing one has zero effect on the other. You can reorder fields for memory efficiency without touching a single json:"..." tag, and your API output stays byte-for-byte identical.
FAQ: Go Struct Field Alignment and Padding
Why is my Go struct bigger than the sum of its field sizes?
Padding. The compiler inserts unused bytes between fields to satisfy each field’s alignment requirement — an int64 needs to start at an address divisible by 8, for example. unsafe.Sizeof() reports the padded total, not the sum of your field sizes. Run go vet -fieldalignment to see exactly how much is padding and what reordering would save.
How do I check the actual memory size of a Go struct?
Call unsafe.Sizeof(YourStruct{}) — it returns the real size in bytes, padding included. To find the cause, check unsafe.Alignof() on each individual field; the field with the largest alignment requirement is almost always the one forcing the gaps elsewhere in the struct.
How Go Allocation Rate Drives GC Pressure and Latency at Scale Stop guessing. Run go tool pprof -alloc_objects to find where your app actually bleeds memory before touching any knobs. Kill heap-escaping pointers. If escape...
Does reordering struct fields change behavior in Go?
No. Field order has zero effect on correctness, zero effect on which fields exist, and zero effect on exported field names. It only changes the struct’s memory layout and total size. JSON serialization, struct comparison, and method behavior are all completely unaffected by reordering.
What is go vet -fieldalignment and why isn’t it enabled by default?
It’s a static analyzer that flags structs which could be smaller with a different field order, and tells you exactly how much smaller. It’s not in the default go vet run because it’s opinionated about memory layout in a way the Go team chose not to enforce universally — many structs simply don’t need this level of scrutiny, so it’s opt-in via a separate install.
Should I always order struct fields from largest to smallest?
As a default heuristic, yes — it minimizes padding in the overwhelming majority of cases. The exception is when a different order meaningfully improves readability or groups logically related fields together. For structs created rarely (config, startup-only types), readability wins easily. For structs created millions of times in a hot path, size wins.
How does struct padding affect CPU cache performance?
A smaller, tightly-packed struct fits more instances into a single CPU cache line. When you iterate over a slice of structs — the most common access pattern in Go — more instances per cache line means fewer cache misses, which translates directly into faster iteration, independent of any change to your actual logic.
Does an embedded interface increase struct size?
Yes, by a fixed amount. An interface value in Go is two words — a pointer to type information and a pointer to the underlying data — regardless of how simple or complex the concrete type behind it is. Embedding an interface field always adds 16 bytes on a 64-bit system, even if the interface wraps something as small as a single bool.
Is struct field alignment optimization worth doing for every struct?
No — be deliberate about where you spend the effort. It’s worth doing for structs instantiated millions of times: queue messages, cache entries, anything stored in large slices. It’s not worth doing for structs created once at startup or rarely during a request’s lifecycle, where the bytes saved are immeasurably small against the readability cost of forcing a specific field order.
Written by: