Lab 01: Memory Model & Escape Analysis

Time: 45 minutes | Level: Advanced | Docker: docker run -it --rm golang:1.22-alpine sh

Overview

Understand Go's memory model, happens-before guarantees, stack vs heap allocation, and escape analysis. Master sync/atomic for lock-free programming.

Prerequisites

  • Go 1.22

  • Basic goroutines and channels knowledge


Step 1: Go Memory Model — Happens-Before

The Go memory model defines when a write to a variable is guaranteed to be visible to a read of that variable.

// happens_before.go
package main

import (
	"fmt"
	"sync"
)

var (
	msg  string
	done bool
	mu   sync.Mutex
)

// WRONG: no synchronization — data race
func unsafeCommunicate() {
	go func() { msg = "hello" }()
	// msg might not be visible here!
	for !done {
	}
	fmt.Println(msg)
}

// CORRECT: channel establishes happens-before
func safeCommunicate() {
	ch := make(chan struct{})
	go func() {
		msg = "hello from goroutine"
		close(ch) // write happens-before close
	}()
	<-ch // close happens-before receive
	fmt.Println(msg) // safe to read
}

func main() {
	safeCommunicate()

	// Mutex also establishes happens-before
	mu.Lock()
	msg = "protected write"
	mu.Unlock()

	mu.Lock()
	fmt.Println(msg) // guaranteed to see "protected write"
	mu.Unlock()
}

💡 Happens-before rule: If event A happens-before event B, then A's effects are visible to B. Channel send happens-before channel receive. sync.Mutex unlock happens-before next lock.


Step 2: Stack vs Heap Allocation

Go automatically decides where to allocate variables — stack (fast, no GC) or heap (GC-managed).


Step 3: Escape Analysis with -gcflags="-m"

📸 Verified Output:

💡 moved to heap: x means the compiler detected x outlives the function stack frame, so it heap-allocates it. Use -gcflags="-m -m" for more detail.


Step 4: sync/atomic — Basic Operations

Run it:

📸 Verified Output:


Step 5: atomic.Pointer[T] — Type-Safe Pointer Swapping


Step 6: Memory Ordering — Relaxed vs Sequential


Step 7: Race Detector

💡 Always run with -race during development and CI. The race detector catches concurrent access bugs at runtime. It adds ~2-20x overhead, so don't use it in production.


Step 8: Capstone — Lock-Free Stack

Build a lock-free LIFO stack using atomic.Pointer and CAS:


Summary

Concept
Key Tool
When to Use

Escape Analysis

go build -gcflags="-m"

Optimize allocations

Atomic Counter

atomic.Int64.Add/Load

High-frequency counters

CAS

CompareAndSwap

Lock-free state machines

Atomic Pointer

atomic.Pointer[T]

Config hot-reload

Race Detection

go run -race

Development & CI

Happens-Before

Channel / Mutex

Guarantee visibility

Key Takeaways:

  • Variables escape to heap when they outlive their stack frame

  • sync/atomic operations are sequentially consistent

  • CAS is the foundation of lock-free data structures

  • Always use the race detector during testing

Last updated