DEV Community

Ratnesh Maurya
Ratnesh Maurya

Posted on • Originally published at blog.ratnesh-maurya.com on

Optimizing Memory Layout in Go: A Deep Dive into Struct Design

#go

Optimizing Memory Layout in Go: A Deep Dive into Struct Design

Reorder the fields in a Go struct and the size changes — without changing the data it holds. A bool next to an int64 wastes 7 bytes of padding. Across 10 million allocations, that's 67MB of memory you're paying for but never using.

This matters in high-throughput services where struct slices dominate heap usage: event pipelines, in-memory caches, analytics collectors.

How alignment and padding work

Go stores struct fields in a contiguous block of memory. Each field must be aligned to a memory address that's a multiple of its own size — int64 aligns to 8 bytes, int32 to 4, bool to 1. When a smaller field is followed by a larger one, the compiler inserts invisible padding bytes to satisfy the alignment requirement.

type Bad struct {
    Active bool // 1 byte
    // 7 bytes padding
    Balance float64 // 8 bytes
    Age uint8 // 1 byte
    // 7 bytes padding
}
// Total: 24 bytes (only 10 bytes of actual data)

Enter fullscreen mode Exit fullscreen mode

Reorder from largest to smallest:

type Good struct {
    Balance float64 // 8 bytes
    Active bool // 1 byte
    Age uint8 // 1 byte
    // 6 bytes padding (struct itself aligns to 8)
}
// Total: 16 bytes (same 10 bytes of data, 8 bytes less waste)

Enter fullscreen mode Exit fullscreen mode

That's a 33% reduction per struct, just by reordering fields.

Measuring the difference

Use reflect.TypeOf and unsafe.Sizeof to check struct sizes at runtime:

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

type Bad struct {
    Active bool
    Balance float64
    Age uint8
}

type Good struct {
    Balance float64
    Active bool
    Age uint8
}

func main() {
    fmt.Println("Bad:", unsafe.Sizeof(Bad{}), "bytes") // 24
    fmt.Println("Good:", unsafe.Sizeof(Good{}), "bytes") // 16

    // Field-by-field inspection
    t := reflect.TypeOf(Bad{})
    for i := 0; i < t.NumField(); i++ {
        f := t.Field(i)
        fmt.Printf(" %s: size=%d, offset=%d\n", f.Name, f.Type.Size(), f.Offset)
    }
}

Enter fullscreen mode Exit fullscreen mode

How much memory this saves at scale

Here's the math for a real scenario — an analytics service tracking page view events:

type PageView struct {
    // Unoptimized layout
    IsBot bool // 1 + 7 padding
    Timestamp int64 // 8
    StatusCode uint16 // 2 + 6 padding
    Duration int64 // 8
    UserID uint32 // 4 + 4 padding
    PathHash uint64 // 8
}
// Size: 48 bytes

type PageViewOptimized struct {
    // Sorted by alignment: 8 → 4 → 2 → 1
    Timestamp int64
    Duration int64
    PathHash uint64
    UserID uint32
    StatusCode uint16
    IsBot bool
}
// Size: 32 bytes

Enter fullscreen mode Exit fullscreen mode
Struct count Unoptimized Optimized Saved
100K 4.6 MB 3.1 MB 1.5 MB
1M 45.8 MB 30.5 MB 15.3 MB
10M 457 MB 305 MB 152 MB

At 10 million structs, the difference is 152MB — enough to matter for your container memory limits and GC pressure.

Automated detection with fieldalignment

You don't need to manually audit every struct. The fieldalignment analyzer from golang.org/x/tools catches suboptimal layouts automatically:

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

Enter fullscreen mode Exit fullscreen mode

It reports every struct that could be smaller and suggests the optimal field order. You can also run it as part of golangci-lint by enabling the govet linter with the fieldalignment check.

The alignment rules

Type Size Alignment
bool, byte, uint8, int8 1 byte 1
uint16, int16 2 bytes 2
uint32, int32, float32 4 bytes 4
uint64, int64, float64, pointer, string, slice, map, interface 8 bytes 8

The general rule: sort fields from largest alignment to smallest. This minimizes padding because smaller fields can pack together in the leftover space after larger fields.

Structs themselves are padded to a multiple of their largest field's alignment. That's why the Good struct above is 16 bytes (multiple of 8) even though the data only needs 10 bytes.

When not to bother

Field ordering optimization is worth the effort when:

  • You allocate millions of the same struct (event pipelines, time-series data, game state)
  • The struct is stored in a large slice that stays in memory
  • You're hitting container memory limits or seeing heavy GC pauses

It's not worth the effort when:

  • The struct is allocated once or a handful of times
  • Readability would suffer from rearranging logically grouped fields
  • The struct is mostly pointers and strings (already 8-byte aligned)

Run fieldalignment on your codebase as a CI check. Fix the easy wins — the structs that save 8+ bytes per instance — and leave the rest alone. The tool does the thinking for you.

Top comments (0)