1) Types of memory
- Code (text) - the part of your program that holds the actual instructions the CPU runs (your functions). It’s read-only/executable code only-no variables or data.
- Globals - package-level variables and constants. Variables you can change live in writable memory (.data if they start non-zero, .bss if they start zero) and are set up before main, while constants never change and their bytes (like string text) sit in read-only memory (.rodata) so you can read them but not modify them.
- Stack - each goroutine’s private call area that holds function parameters, locals, and return addresses. it grows/shrinks automatically as you call/return (LIFO). It is very fast, and data there doesn’t outlive the function.
- Heap - dynamic allocations that outlive scope. Managed by the runtime.
2) Escape Analysis
Go compiler decides storage based on escape analysis.
Mental model
- Locals by value that never leak => stack.
- Locals whose address escapes (e.g. returned pointer) => heap.
- Inspect with:
go build -gcflags="all=-m"
(look for escapes to heap).
Example
package main
import "fmt"
type Box struct{ W, H int }
// stack: returned by value
func MakeBox(w, h int) Box {
b := Box{W: w, H: h}
return b
}
// heap: pointer escapes
func NewBox(w, h int) *Box {
b := Box{W: w, H: h}
return &b
}
func main() {
a := MakeBox(2, 3)
p := NewBox(4, 5)
fmt.Println(a, p)
}
3) Struct Padding & Alignment
How field order, offsets, and alignment rules determine memory size.
Definitions
- Alignment - How even the address must be.
unsafe.Alignof(x)
returns 1, 2, 4, 8, … and a value must live at an address divisible by that number (e.g.,uint32
=> 4,uint64
=> 8). A struct’s alignment is the largest alignment of any of its fields. - Offset - Where a field starts inside its struct, in bytes from the start (0, 1, 2, …).
unsafe.Offsetof(s.F)
includes any padding beforeF
and is a compile-time constant. - Size - How many bytes the value itself occupies.
unsafe.Sizeof(x)
for structs counts all field bytes plus padding to the end. It does not include data behind pointers/slices/maps/strings-only their fixed-size headers (e.g., a string header is 16 bytes on 64-bit).
Formal rules
- Field offset
off
must satisfy:off % Alignof(field) == 0
. - Struct alignment = max alignment of its fields.
- Struct size = rounded up to multiple of its alignment.
- Padding can be interior (between fields) or tail (at end of struct).
Why padding is necessary
Alignment puts data where the CPU expects it. That makes reads/writes fast and safe. misalignment can slow things down or even crash, and alignment also keeps Go and C layouts compatible for cgo.
Bad vs Good layout
package main
import (
"fmt"
"unsafe"
)
// Bad layout: wasted padding
type Bad struct {
A uint8 // 1B
B uint64 // align 8 => needs pad after A
C uint8 // 1B => pad at end
}
// Good layout: large fields first
type Good struct {
B uint64
A uint8
C uint8
}
func main() {
fmt.Println("Bad size:", unsafe.Sizeof(Bad{}), "align:", unsafe.Alignof(Bad{}))
fmt.Println("Good size:", unsafe.Sizeof(Good{}), "align:", unsafe.Alignof(Good{}))
}
// Bad = 24B, Good = 16B
Descriptor sizes
package main
import (
"fmt"
"unsafe"
)
func main() {
var p *int
s := "hi"
sl := make([]int, 0, 8)
m := make(map[int]int)
ch := make(chan int, 1)
var i any = 123
fmt.Printf("*T pointer size=%d align=%d\n", unsafe.Sizeof(p), unsafe.Alignof(p))
fmt.Printf("string size=%d align=%d (len=%d)\n", unsafe.Sizeof(s), unsafe.Alignof(s), len(s))
fmt.Printf("[]int (slice) size=%d align=%d (len=%d cap=%d)\n", unsafe.Sizeof(sl), unsafe.Alignof(sl), len(sl), cap(sl))
fmt.Printf("map[int]int size=%d align=%d\n", unsafe.Sizeof(m), unsafe.Alignof(m))
fmt.Printf("chan int size=%d align=%d\n", unsafe.Sizeof(ch), unsafe.Alignof(ch))
fmt.Printf("interface{} (any) size=%d align=%d\n", unsafe.Sizeof(i), unsafe.Alignof(i))
}
Sizeof for pointers, strings, slices, maps, chans, interfaces
Nested struct offsets
package main
import (
"fmt"
"unsafe"
)
type Inner struct {
U16 uint16
B byte
}
type Outer struct {
B byte
In Inner
U32 uint32
}
func main() {
var o Outer
fmt.Println("Alignof(Inner):", unsafe.Alignof(Inner{}))
fmt.Println("Alignof(Outer):", unsafe.Alignof(Outer{}))
fmt.Println("Offset B :", unsafe.Offsetof(o.B))
fmt.Println("Offset In :", unsafe.Offsetof(o.In))
fmt.Println("Offset U32 :", unsafe.Offsetof(o.U32))
fmt.Println("Sizeof(Inner):", unsafe.Sizeof(Inner{}))
fmt.Println("Sizeof(Outer):", unsafe.Sizeof(Outer{}))
}
// Alignof(Inner): 2
// Alignof(Outer): 4
// Offset B : 0
// Offset In : 2
// Offset U32 : 8
// Sizeof(Inner): 4
// Sizeof(Outer): 12
Empty struct
package main
import (
"fmt"
"unsafe"
)
func main() {
type Empty struct{}
fmt.Println("Sizeof Empty:", unsafe.Sizeof(Empty{})) // 0
}
struct{}
has size 0. In C++ it is 1 byte to ensure distinct addresses. Go can overlay multiple struct{}
values at the same address, perfect for zero-cost markers in maps/channels.4) Endianness
Endianness refers to the order in which bytes or smaller units of data are stored in a computer's memory or transmitted over a network.
Unsafe pointer casts: how to reinterpret memory
unsafe.Pointer
lets you look at the same memory as if it were a different type. Nothing is copied - you’re just reinterpreting the bytes at the same address. Use this only when you really need low-level access.
How to do it (plain steps):
- Take the address of a value (a normal pointer).
- Convert that pointer to unsafe.Pointer.
- Convert the unsafe.Pointer to the new pointer type you want.
- Read carefully - writing through a reinterpreted pointer can be dangerous.
Examples:
package main
import (
"fmt"
"unsafe"
)
func main() {
var n uint32 = 0x11333311
// Reinterpret the uint32 as an array of 4 bytes
bytes := (*[4]byte)(unsafe.Pointer(&n))
fmt.Printf("Raw bytes: % x\n", bytes)
b1, b2, b3, b4 := bytes[0], bytes[1], bytes[2], bytes[3]
left := b1 % b4
right := b2 % b3
fmt.Printf("(%d %% %d) = %d, (%d %% %d) = %d\n", b1, b4, left, b2, b3, right)
if left == right {
fmt.Println("Condition holds")
} else {
fmt.Println("Condition fails")
}
}
package main
import (
"fmt"
"unsafe"
)
func main() {
var f float64 = 3.141592653589793
// View the exact same 8 bytes as a uint64 (bit-level reinterpretation).
u := *(*uint64)(unsafe.Pointer(&f))
fmt.Printf("float64=%v and rawBits=0x%016x\n", f, u)
// Reinterpret u's bytes as a float64
f2 := *(*float64)(unsafe.Pointer(&u))
fmt.Println("round-trip:", f2)
}
package main
import (
"fmt"
"unsafe"
)
type Header struct {
ID uint32
Flag uint16
Pad byte
}
func main() {
h := Header{ID: 0xA1B2C3D4, Flag: 0x7788}
// Treat &h as *[N]byte to inspect its in-memory bytes (including padding).
n := unsafe.Sizeof(h)
b := (*[32]byte)(unsafe.Pointer(&h)) // big enough static bound
fmt.Printf("size=%d bytes: % x\n", n, b[:n])
}
Detect endianness
package main
import (
"fmt"
"unsafe"
)
// isLittleEndian reports whether the current machine uses little-endian byte order.
func isLittleEndian() bool {
var x uint16 = 0x0102
b := (*[2]byte)(unsafe.Pointer(&x)) // treat x's address as 2 raw bytes
// If the first byte in memory is the low byte (0x02), we're little-endian.
// If it's 0x01, we're big-endian.
return b[0] == 0x02
}
func main() {
fmt.Println("Is little endian: ", isLittleEndian())
}