Field Alignment and Padding

Understanding how Go arranges struct fields in memory, why padding bytes exist, and how to optimize field ordering for size and performance.

When you declare a struct, the fields are not simply packed together end‑to‑end. The Go compiler inserts invisible padding bytes between some fields—and often after the last field—to satisfy the CPU’s memory alignment requirements. These invisible gaps affect the total size of the struct and, when you have millions of instances, the difference can be measured in megabytes of wasted memory and slower cache access.

This document explains why alignment exists, how you can inspect the real layout of your structs, and the simple reordering rules that let you shrink struct sizes without changing a single line of logic.

What Alignment Means for a Single Field

Every data type in Go has an alignment requirement: a number that tells the compiler the memory address of a value must be a multiple of that number. If an int64 requires 8‑byte alignment, the compiler must place it at an address divisible by 8.

You can query the alignment of a type using unsafe.Alignof. On a 64‑bit system the results follow a predictable pattern:

TypeAlignment (bytes)
bool, byte, int8, uint81
int16, uint162
int32, uint32, float324
int64, uint64, float648
int, uint, uintptr8
string (header)8
slice (header)8
map, chan, func, pointer8

The alignment of a struct itself is the largest alignment among its fields. For a struct containing an int64, the whole struct must be 8‑byte aligned, even if the first field is a 1‑byte bool.

How the Compiler Lays Out Struct Fields

The compiler walks through the fields in declaration order, placing each at the lowest offset that satisfies its alignment. Any unused bytes between the end of the previous field and that offset become padding.

Consider this struct with three fields:

type Misaligned struct {
    A byte   // offset 0, size 1
    B int64  // alignment 8 → must start at offset 8
    C byte   // offset 16 (size 1)
}

The compiler inserts 7 bytes of padding between A and B so that B starts at offset 8. After the final C the struct size must be a multiple of the struct’s own alignment (8). So it adds 7 bytes of trailing padding. The total size becomes 24 bytes, though the fields themselves need only 10.

Use unsafe.Sizeof and unsafe.Offsetof to measure this directly:

package main
import (
    "fmt"
    "unsafe"
)
func main() {
    type Misaligned struct {
        A byte
        B int64
        C byte
    }
    var s Misaligned
    fmt.Println("Size:", unsafe.Sizeof(s))        // 24 on 64-bit
    fmt.Println("Offset A:", unsafe.Offsetof(s.A)) // 0
    fmt.Println("Offset B:", unsafe.Offsetof(s.B)) // 8
    fmt.Println("Offset C:", unsafe.Offsetof(s.C)) // 16
}

The offsets reveal exactly where padding sits. A 7‑byte gap after offset 1, and another 7‑byte gap after offset 17 to reach size 24.

End‑of‑struct padding:

Trailing padding ensures that when you place structs back‑to‑back in an array or slice, each element starts at the correct alignment for its first field. Without it, arr[1].B might sit on a misaligned address.

Why Padding Exists

CPUs read memory in aligned words (typically 8 bytes on modern 64‑bit chips). If a value straddles a word boundary the CPU may need two separate memory fetches, merge the bytes, and then proceed—a measurable slowdown. On some older architectures unaligned access would even cause a hardware fault.

Go prevents this by always placing each field at an address that is a multiple of its alignment, even if that means adding unused bytes. Padding is not a compiler quirk; it is the mechanism that keeps memory access fast and predictable on all supported platforms.

Go handles this automatically:

You never have to manually compute offsets or insert padding for correctness. The compiler enforces the platform’s alignment rules. The only thing you might want to do is reorder fields to reduce the padding the compiler must add.

Optimizing Layout by Reordering Fields

The amount of padding depends on the order of the fields. When a large‑alignment field follows a small one, the compiler must skip ahead to the next aligned address. By placing fields with the largest alignment first, the smaller fields naturally fill the gaps, often eliminating padding completely.

Compare the same three fields in two different orders:

type Unoptimized struct {
    A byte   // 1 byte + 7 padding
    B int64  // 8 bytes
    C byte   // 1 byte + 7 trailing padding
} // 24 bytes
type Optimized struct {
    B int64  // 8 bytes
    A byte   // 1 byte
    C byte   // 1 byte
} // 16 bytes (still 8‑aligned)

Running unsafe.Sizeof on both confirms the difference: 24 bytes versus 16 bytes—a one‑third reduction with zero logic change.

This pattern scales with every field you add. A struct with many mixed‑size fields can waste dozens of bytes per instance if the order is scattered. The general guideline is:

  • Place slices ([]T), strings, and interfaces first (the largest headers).
  • Next, 8‑byte fields: int64, float64, pointers, maps, channels, functions.
  • Then 4‑byte fields: int32, float32.
  • Then 2‑byte: int16, uint16.
  • Finally, 1‑byte fields: bool, byte, int8.

Grouping fields of the same size together lets the compiler pack them tightly.

Reordering fields can break unkeyed struct literals:

If existing code uses positional struct literals like MyStruct{1, true, "x"}, changing field order will silently assign the wrong values. Always use keyed literals (MyStruct{ID: 1, Active: true, Name: "x"}) before reordering fields in production types.

Using Blank Fields for Explicit Padding

Sometimes padding is not a waste to be removed—it is a tool you add intentionally. Go allows zero‑width blank fields inside structs, but they do not occupy memory. However, you can use sized blank fields to create deliberate gaps.

The most common reason is false sharing in concurrent code. Modern CPUs load data in cache lines (typically 64 bytes). If two goroutines modify different fields of the same struct that happen to reside on the same cache line, each write invalidates the line in the other core’s cache, forcing expensive reloads—even though the fields are logically independent.

By inserting padding between the fields, you force them onto separate cache lines:

type SharedCounters struct {
    a int64        // 8 bytes
    _ [56]byte     // explicit padding to reach 64 bytes
    b int64        // 8 bytes — now on a different cache line
}

Now two goroutines can increment a and b in parallel without contending over the same cache line.

Blank field padding is highly architecture‑dependent:

Cache line size varies across platforms (64 bytes on x86/arm64, but 128 bytes on Apple M‑series chips in some scenarios). This technique is a micro‑optimisation that should be applied only after profiling confirms false sharing is a bottleneck, and it must be tested on the actual target hardware.

Blank fields can also be used to force 8‑byte alignment of a subsequent 64‑bit field on 32‑bit platforms, as required by sync/atomic. The first word in a struct is always aligned to the architecture’s word size, but if a 64‑bit field appears later, a blank uint32 field before it can push it to the right boundary. However, the simplest way to guarantee alignment is to place the 64‑bit field as the very first field of the struct.

Inspecting Layout Programmatically

Aside from unsafe.Sizeof and unsafe.Offsetof, you can use the official fieldalignment analyzer to scan an entire codebase for suboptimal struct layouts.

Install it once:

go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

Run it on your package:

fieldalignment ./...

It reports structs whose fields could be reordered to save memory, and suggests the optimal field order. Integrating this into CI prevents drift over time.

Common Misconceptions

“The Go compiler will reorder fields for me.” Go preserves the exact field order you write. It will never rearrange fields to save space. The responsibility for an efficient layout is yours.

“A struct with only small fields has no padding.” Even a struct of byte and int32 will contain padding if the int32 isn’t already at a 4‑byte boundary. Padding is driven by the largest alignment in the struct, not by the size of the smallest field.

struct{} fields add no size.” They have size 0, but a struct{} field with alignment 1 can still affect offsets. If a zero‑width field sits between two larger fields, the compiler may still leave gaps. This rarely matters, but it can surprise you when inspecting offsets.

“Reordering fields is always worth the effort.” For a struct that appears only a few times in a program, the savings are negligible. The effort pays off when the struct is held in large slices, maps, or is allocated millions of times.

When Alignment Tuning Matters

  • High‑volume data structures: caches, logs, metrics, in‑memory queues.
  • Binary serialization: any format that uses unsafe or direct memory copies (though this is rare in pure Go; encoding/gob and protobuf are safe).
  • Concurrent hot paths: where false sharing may degrade throughput.
  • Embedded systems and memory‑constrained environments: every byte counts.

In a typical web service, the difference is often invisible. But when your heap profile shows larger‑than‑expected allocations, a quick check with fieldalignment can uncover easy wins.

Summary

Struct field alignment and padding are a direct consequence of how CPUs access memory. Go’s compiler automatically inserts padding to keep field access fast and correct, but the amount of padding depends entirely on the order you choose for your fields.

Reorder fields from largest alignment to smallest, group similar‑sized types, and use keyed literals to keep refactoring safe. Blank fields give you a way to add intentional padding when false sharing becomes a measured problem, but they are a sharp tool best left for profiler‑guided optimisation.

When you embed one struct inside another, the compiler lays out the embedded fields according to the same alignment rules—so a compact inner struct helps the outer struct stay compact too.