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

posted Originally published at blog.ratnesh-maurya.com 5 min read

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)

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)

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)
    }
}

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
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 ./...

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.

More Posts

Optimizing the Clinical Interface: Data Management for Efficient Medical Outcomes

Huifer - Jan 26

How I Built a React Portfolio in 7 Days That Landed ₹1.2L in Freelance Work

Dharanidharan - Feb 9

Kubernetes HPA Best Practices: When CPU Works, Why Memory Almost Never Does

Alexandre Vazquez - Apr 13

Legacy in the Data: Transforming Family Medical History into a Blueprint for Longevity

Huifer - Jan 29

Memory is Not a Database: Implementing a Deterministic Family Health Ledger

Huifer - Jan 21
chevron_left

Related Jobs

View all jobs →

Commenters (This Week)

7 comments
4 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!