Right, let’s talk about what your computer actually does when you define a struct. It’s not just neatly stacking your fields in a row like a perfectly organized bookshelf. It’s more like a Tetris game played by a slightly obsessive-compulsive robot whose only goal is to make the CPU’s life easier, even if it wastes a bit of memory in the process. This is the world of memory alignment and padding, and if you ignore it, you can accidentally write code that’s hilariously inefficient.

The CPU doesn’t like to work harder than it has to. It prefers to read data from memory in chunks that are the same size as its data bus (often 4, 8, or 16 bytes). To make this efficient, it requires that data values be stored at memory addresses that are multiples of their own size. A uint32 (4 bytes) should live at an address divisible by 4. A uint64 (8 bytes) wants an address divisible by 8. This is alignment. If the data isn’t aligned, the CPU might have to perform two separate, slower memory reads and stitch the result together—a performance penalty known as a “bus error” on some architectures (though modern x86 chips mostly handle it with a hidden performance cost).

Go’s compiler, being a good friend to the CPU, will automatically add padding—empty bytes—to your struct fields to ensure every field is properly aligned. This is where the magic (and the wasted space) happens.

The Cost of a Bad Layout

Let’s look at a classic example. Behold, the Wasteful struct.

package main

import (
	"fmt"
	"unsafe"
)

type Wasteful struct {
	a bool    // 1 byte
	b int64   // 8 bytes
	c bool    // 1 byte
}

func main() {
	w := Wasteful{}
	fmt.Printf("Total size: %d bytes\n", unsafe.Sizeof(w))
	fmt.Printf("a: offset %d, size %d\n", unsafe.Offsetof(w.a), unsafe.Sizeof(w.a))
	fmt.Printf("b: offset %d, size %d\n", unsafe.Offsetof(w.b), unsafe.Sizeof(w.b))
	fmt.Printf("c: offset %d, size %d\n", unsafe.Offsetof(w.c), unsafe.Sizeof(w.c))
}

Run this. Go on, I’ll wait. You’ll likely see something beautiful and tragic:

Total size: 24 bytes
a: offset 0, size 1
b: offset 8, size 8
c: offset 16, size 1

24 bytes! For 10 bytes of actual data! Let’s break down the compiler’s thought process:

  1. a (bool, 1 byte) goes at offset 0. Easy.
  2. Next up: b (int64, 8 bytes). It needs an address divisible by 8. The next available address is 1. 1 is not divisible by 8. So the compiler adds 7 bytes of padding and places b at offset 8.
  3. c (bool, 1 byte) goes next at offset 16.
  4. Now, the whole struct itself must be aligned to the largest field’s requirement (int64, 8 bytes). The total size so far is 17 bytes. The next multiple of 8 after 17 is 24, so the compiler slaps on another 7 bytes of padding at the end.

It’s like packing a single frying pan in a huge box filled with styrofoam peanuts. The shipper (CPU) is happy, but you’re paying for a lot of air.

Fixing the Layout Manually

Now, let’s be smarter than the compiler and group our fields by their inherent alignment needs.

type Efficient struct {
	b int64   // 8 bytes (align 8)
	a bool    // 1 byte (align 1)
	c bool    // 1 byte (align 1)
}

func main() {
	e := Efficient{}
	fmt.Printf("Total size: %d bytes\n", unsafe.Sizeof(e)) // Total size: 16 bytes
	fmt.Printf("b: offset %d\n", unsafe.Offsetof(e.b)) // b: offset 0
	fmt.Printf("a: offset %d\n", unsafe.Offsetof(e.a)) // a: offset 8
	fmt.Printf("c: offset %d\n", unsafe.Offsetof(e.c)) // c: offset 9
}

16 bytes. We just saved 8 bytes (33%!) per instance without changing a single line of logic. How?

  1. Place the largest field (b, 8 bytes) first at offset 0. Perfect alignment.
  2. The two bool fields (a and c, 1 byte each) can happily live in the next two bytes (offsets 8 and 9). They only need alignment to 1, which is always true.
  3. The total size is 10 bytes. The struct’s alignment is still dictated by the largest field (int64, 8 bytes). The next multiple of 8 after 10 is 16, so we get 6 bytes of trailing padding.

This isn’t a silver bullet. The rule of thumb is simple: order your fields from largest to smallest. It’s the single biggest trick to minimize padding in most cases. Your program might not care about 8 bytes, but if you’re creating millions of these in a slice for a high-performance data processing task, the difference in memory footprint and cache efficiency is staggering.

The Weird Edge Cases

It’s not always this straightforward. The presence of a field smaller than a machine word (like an int32 on a 64-bit system) can create “holes” you might not expect.

type Tricky struct {
	a int32     // 4 bytes (align 4)
	b bool      // 1 byte (align 1)
}
// Size is likely 8 bytes, not 5.

Why? The int32 demands 4-byte alignment for the struct itself. The total data is 5 bytes, so the next multiple of 4 is 8. Padding strikes again.

The takeaway? Don’t guess. Use the unsafe package’s Sizeof and Offsetof to empirically verify your struct’s layout when it absolutely matters. Profile your application. If you’re not memory-bound, maybe don’t obsess over it. But knowing why it happens means you’re no longer just throwing structs at the wall and hoping they stick. You’re designing them.