Runtime Internals: Reading Go From the Inside
What this module is for
Section titled “What this module is for”Every earlier module in this course taught you to write Go: syntax, types, methods, concurrency primitives, the standard library. None of it required you to know how any of it actually runs. You can write a working goroutine, a correct sync.Mutex, a struct with a dozen fields, without ever asking what the scheduler does with the first, what “correct” even means for the second, or how the third is arranged in memory.
That gap is exactly where AI-generated Go fails most often — and where a reviewer who has never seen the machinery underneath fails to catch it. A generated snippet can be syntactically perfect, pass go vet, and still leak a goroutine, race on a shared flag, or allocate on every call in a hot loop. None of those are syntax errors. All three are runtime facts.
This module has one job: give you the four pieces of runtime knowledge that let you look at someone else’s Go source — not run it, not profile it, just read it — and answer three questions with confidence:
- Does this allocate? (escape analysis, memory layout)
- Can this race? (the memory model, happens-before)
- Can this leak or block forever? (the scheduler, goroutine lifecycle)
What this module covers
Section titled “What this module covers”| Lesson | Answers the question |
|---|---|
| Memory Model | When is a write in one goroutine guaranteed visible to a read in another? |
| Scheduler | How does a goroutine actually get onto a CPU core, and what can stall it? |
| GC and Escape Analysis | Does this value live on the stack or the heap, and what does the collector do with it? |
| Memory Layout | How many bytes does this value actually take, and how is it arranged? |
| Cgo and FFI | What happens to the memory model, the scheduler, and the GC’s view of the world when Go code calls into C? |
The Goroutines and Sync Primitives lessons already introduced the GMP model, work stealing, async preemption, and the memory model’s basic shape — this module goes past the introduction into the mechanism, and links back rather than repeating them. The Pointers lesson introduced escape analysis at the level of “why a pointer isn’t always faster”; GC and Escape Analysis goes into what the collector does with what escapes. The Interfaces lesson already showed the two-word interface value and the itab; Memory Layout puts a byte count on every type, not just interfaces.
A runnable question: what does the runtime already know about your program?
Section titled “A runnable question: what does the runtime already know about your program?”Before any of the deeper mechanism, the runtime package itself answers simple version of all three questions from inside a running program — no profiler needed.
package main
import ( "fmt" "runtime" "sync")
func main() { fmt.Println("Go version:", runtime.Version()) fmt.Println("GOARCH/GOOS:", runtime.GOARCH, runtime.GOOS) fmt.Println("NumCPU:", runtime.NumCPU()) fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) fmt.Println("goroutines before:", runtime.NumGoroutine())
var wg sync.WaitGroup block := make(chan struct{}) for i := 0; i < 3; i++ { wg.Add(1) go func() { defer wg.Done() <-block // parked, not spinning on CPU }() } fmt.Println("goroutines after launch:", runtime.NumGoroutine()) close(block) wg.Wait() fmt.Println("goroutines after Wait:", runtime.NumGoroutine())} Real output on darwin/arm64, 11 logical CPUs:
Go version: go1.27.0GOARCH/GOOS: arm64 darwinNumCPU: 11GOMAXPROCS: 11goroutines before: 1goroutines after launch: 4goroutines after Wait: 1runtime.NumGoroutine() counting 4 after launching 3 (the main goroutine plus three parked ones) and dropping back to 1 after wg.Wait() is the cheapest leak check that exists: snapshot the count before and after a piece of code runs, and assert it returns to baseline. You will use exactly this trick again in the Scheduler lesson.
Mental Model
Section titled “Mental Model”flowchart TD
subgraph Left["Your only view of unfamiliar Go"]
Src["source code"] -->|"does this look right?"| Compiles["compiles, vets clean"]
Compiles --> Idiomatic["looks idiomatic"]
end
subgraph Right["What actually decides behavior"]
MM["memory model (happens-before)"] -->|"is this write visible elsewhere?"| Sched["scheduler (GMP)"]
Sched -->|"does this goroutine ever run again?"| GCEA["GC + escape analysis"]
GCEA -->|"does this allocate, and when is it freed?"| Layout["memory layout (how many bytes, how aligned?)"]
end
Compiles --> Sched Reading Go well means running this right-hand column in your head while your eyes are only doing the left-hand one.
Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| Knowing the memory model | You can spot a race without running -race | It takes real study — happens-before is not “same line order” |
| Knowing the scheduler | You can predict when a goroutine blocks vs. leaks | GMP has real depth (local/global queues, netpoller, sysmon) |
| Knowing escape analysis | You can predict allocations by reading, not just benchmarking | The compiler’s actual reasoning changes across Go releases |
| Knowing memory layout | You can spot wasteful struct padding on sight | Only matters at scale — premature layout tuning wastes readability |
Common Misconceptions
Section titled “Common Misconceptions”- “It compiled and
go vetpassed, so it’s fine” —go vetand the compiler check syntax and a fixed set of known mistakes; they do not check whether your program races, leaks a goroutine, or allocates needlessly. All three compile cleanly. - “Runtime internals are for people writing the Go compiler” — you need enough of this to read, not to reimplement the scheduler. Every fact in this module is something you can point to in someone else’s diff.
- “The race detector always tells you if code is safe” — it only reports races that actually happen during the run it instruments; a code path a test never exercises can still race in production. The Memory Model lesson covers exactly what
-racedoes and does not catch. - “Profiling is the only way to know if something allocates” — profiling confirms; reading
-gcflags='-m -m'output or knowing the four or five shapes that force an escape lets you predict it from the source, which is the point of this module.
Reading AI-generated code
Section titled “Reading AI-generated code”The whole point of this module is this section, four times over — one per lesson. As a preview, here is the survey-level version: a snippet that compiles, passes go vet, and is still wrong in a way none of the earlier modules in this course would catch.
🐛 Spot the bug
func recordHit(counts map[string]int, key string) { go func() { counts[key]++ // no lock, no channel, called from many goroutines }()}Show the answer
This compiles. It passes go vet. Nothing in the Basics, Types, or Methods modules flags it. But counts[key]++ from an unsynchronized goroutine, called concurrently from more than one place, is a data race on a plain Go map — which is not even safe for one writer plus any reader at the same time, let alone two writers. Two goroutines can genuinely corrupt the map’s internal structure, and Go’s runtime detects this specific case and crashes with fatal error: concurrent map writes rather than silently corrupting data — but only if you get lucky enough to trigger the runtime’s own check. The Memory Model lesson in this module explains exactly why “no shared mutable state” has to be enforced by you, not inferred from the code compiling.
func recordHit(mu *sync.Mutex, counts map[string]int, key string) { mu.Lock() defer mu.Unlock() counts[key]++}💡 Real-world example
sync.Map’s own doc comment (sync/map.goin the standard library) explains exactly when a plainmapplus aMutexbeats it and when it does not — a decision that only makes sense once you know what a data race actually is and what it costs to prevent one. The next four lessons build that foundation.