← All posts

Why Go struct field order matters in memory

A look at how Go struct layout turns field order into hidden padding, and how Go Struct Field Alignment — offsets and padding makes those costs inspectable. The useful part is not the reorder alone, but the ability to verify offsets, total size, and architecture-specific behavior against Go itself.

Memory layout bugs are easy to miss in Go because the source code looks clean while the compiled layout tells a different story. Field order affects padding. Padding affects size. Size affects cache use, memory pressure, and the cost of moving values around. If you work with large slices of structs, hot paths, or wire-facing data types, a few bytes per value add up fast.

Go Struct Field Alignment — offsets and padding focuses on one narrow problem. It shows where each field lands in memory, how much padding sits between fields, the total size of the struct, and what a reordered layout would save for the architecture you choose. That scope matters. It keeps the result inspectable. You are not asked to trust a black box. You can compare the output to Go’s own layout rules and verify each claim.

For practitioners, this kind of tool is useful for two reasons. First, it makes an invisible cost visible. Second, it helps you separate meaningful optimization from cargo-cult cleanup. Reordering fields is simple. Knowing when it matters takes more care. The engineering value is in exposing the exact offsets and alignment constraints so you can decide with context.

What the tool is inspecting

Go lays out struct fields in declaration order. Each field has an alignment requirement. The compiler inserts padding when the next field would otherwise start at an offset that breaks that requirement. The struct as a whole then gets rounded up so its size is a multiple of its largest field alignment.

Those rules produce a few common patterns:

  • Small fields between large fields create internal padding.
  • A small trailing field often leads to end padding.
  • Nested structs carry their own alignment needs into the outer layout.
  • The same source type changes size across architectures because pointer size and alignment differ.

A practical example makes this concrete:

type Example struct {
    A bool   // 1 byte
    B int64  // 8-byte alignment on common 64-bit targets
    C int32  // 4-byte alignment
}

A human reading this sees three fields. The machine sees alignment boundaries. A starts at offset 0. B cannot start at offset 1, so padding fills the gap until the next valid boundary. C follows B, then the whole struct gets rounded up again if needed. A different order often shrinks the total size.

The tool surfaces this directly:

  • Per-field offset
  • Padding between fields
  • Total size
  • A reordered version
  • The saving from that reorder
  • The selected architecture as part of the calculation

This is the right level of detail. It tells you what changed and why.

How to verify the output yourself

A layout tool should be easy to check without special infrastructure. In Go, you have a few direct ways to do this.

Start with the standard library. unsafe.Offsetof reports field offsets. unsafe.Sizeof reports the size of values and types as laid out by the compiler for your target build.

package main

import (
    "fmt"
    "unsafe"
)

type Example struct {
    A bool
    B int64
    C int32
}

func main() {
    var x Example
    fmt.Println("A offset:", unsafe.Offsetof(x.A))
    fmt.Println("B offset:", unsafe.Offsetof(x.B))
    fmt.Println("C offset:", unsafe.Offsetof(x.C))
    fmt.Println("size:", unsafe.Sizeof(x))
}

If the tool reports a reordered version, define the reordered struct and compare sizes the same way. You do not need to inspect assembly or runtime internals for first-pass validation.

You should also verify on the architecture you care about. A layout checked on one target does not answer for another. The subject’s architecture selector matters because int, uint, pointers, and alignment-sensitive composites vary by target. If your code ships to multiple targets, inspect each one rather than assuming a single answer travels everywhere.

For code review, a useful workflow is simple:

  1. Identify a struct used in large collections or high-frequency paths.
  2. Paste it into the tool for the relevant architecture.
  3. Compare current layout with the suggested reorder.
  4. Confirm offsets and size in a small Go program.
  5. Decide whether the readability tradeoff is worth the size reduction.

That last step is important. Smaller is not always better if it obscures meaning in a widely read type.

Signals worth reading in the result

A field-alignment report is most useful when you know what patterns matter.

Look first for large internal gaps. They often come from alternating small and large fields, such as bool, then int64, then bool, then another aligned type. A reorder which groups wider fields first often removes several holes at once.

Look next for end padding. This is easy to miss because there is no field after it. The struct size still grows. If a report shows a trailing waste region, a different order might absorb smaller fields into space that would otherwise be lost.

Then look at arrays and slices of the struct. A one-byte saving on a single instance feels trivial. In a slice with many elements, each element carries the full struct stride. The repeated cost matters more than the isolated object view.

Also inspect semantic groupings. Some teams order fields by meaning. Others put hot fields together to improve locality for the access pattern they care about. A pure size-minimizing reorder is a good baseline, not an automatic final answer. You still need to read the type in the context of the code around it.

A few signals deserve extra attention:

  • bool fields scattered between wide numeric or pointer fields
  • Mixed use of byte, int16, int32, int64, and pointers
  • Embedded structs with their own padding
  • Types copied frequently by value
  • Structs used as map values or sent across channels by value

These are the cases where hidden bytes tend to matter more.

Where this class of system commonly goes wrong

The first failure mode is oversimplifying Go’s type rules. Any tool in this space needs to model architecture-specific sizes and alignment correctly. If it treats all integers alike, ignores pointer width, or forgets final struct rounding, the output looks plausible while being wrong.

The second failure mode is treating a parser as a compiler. Real Go structs include tags, embedded fields, nested types, imported names, arrays, and architecture-sensitive built-ins. A brittle parser breaks on valid input or, worse, accepts it and computes the wrong layout. A reader should test edge cases before trusting broad use.

The third failure mode is presenting a reorder as universal advice. A smaller layout is one dimension. It does not account for code clarity, binary compatibility concerns, field grouping by purpose, or serialization expectations in code that reflects over field order. Reordering exported fields in widely used types also creates churn for little gain if the struct is not performance-sensitive.

Another common mistake is ignoring the difference between in-memory layout and external encoding. JSON field order, database column order, and protobuf or other schema identifiers follow their own rules. An engineer should never infer wire compatibility from memory layout output.

There is also a benchmarking trap. Engineers see a smaller struct and assume a speedup. Sometimes you get one. Sometimes you only reduce memory use. Sometimes the effect disappears in the noise because the hot cost sits elsewhere. Layout inspection should inform measurement, not replace it.

How to use it well in practice

The best use of a struct-alignment tool is targeted review. Start with types that appear many times, move through hot code paths, or sit in memory for long periods. Those types give you the clearest payoff from inspection.

Pair the report with a quick measurement plan:

  • Check bytes per object before and after.
  • If the type lives in a slice, estimate stride reduction across the collection.
  • If the type is copied often, look at allocations and CPU profiles in the surrounding code.
  • If the type is part of an API model, weigh readability and maintenance cost before changing it.

Keep an eye on false savings. Reordering a small struct with few instances rarely matters. Reordering a core element type used across large heaps often does. The value comes from choosing the right target, not from applying the same edit everywhere.

This also works well as a teaching aid. New Go developers often know padding exists but do not have a mental model for where it appears. Showing offsets and explicit padding turns a vague compiler detail into something concrete and testable.

What to watch next

The next step is context. Layout is one part of data-shape engineering. After you inspect a struct, look at how it is allocated, copied, iterated, and serialized. A leaner layout helps most when it matches the way your code uses the type.

For this tool, the key thing to watch is whether the reported savings line up with compiler-observed size on the architecture you ship. If they do, you have a reliable basis for review. If a reorder saves space, measure in the calling code before and after. That is where a neat memory diagram turns into an engineering decision.