Skip to content

Runtime Internals: Reading Go From the Inside

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:

  1. Does this allocate? (escape analysis, memory layout)
  2. Can this race? (the memory model, happens-before)
  3. Can this leak or block forever? (the scheduler, goroutine lifecycle)
LessonAnswers the question
Memory ModelWhen is a write in one goroutine guaranteed visible to a read in another?
SchedulerHow does a goroutine actually get onto a CPU core, and what can stall it?
GC and Escape AnalysisDoes this value live on the stack or the heap, and what does the collector do with it?
Memory LayoutHow many bytes does this value actually take, and how is it arranged?
Cgo and FFIWhat 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.0
GOARCH/GOOS: arm64 darwin
NumCPU: 11
GOMAXPROCS: 11
goroutines before: 1
goroutines after launch: 4
goroutines after Wait: 1

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

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 source tells you it compiles and looks idiomatic; the memory model, scheduler, GC, and memory layout are what actually decide behavior

Reading Go well means running this right-hand column in your head while your eyes are only doing the left-hand one.

What you getBenefitCost
Knowing the memory modelYou can spot a race without running -raceIt takes real study — happens-before is not “same line order”
Knowing the schedulerYou can predict when a goroutine blocks vs. leaksGMP has real depth (local/global queues, netpoller, sysmon)
Knowing escape analysisYou can predict allocations by reading, not just benchmarkingThe compiler’s actual reasoning changes across Go releases
Knowing memory layoutYou can spot wasteful struct padding on sightOnly matters at scale — premature layout tuning wastes readability
  • “It compiled and go vet passed, so it’s fine” — go vet and 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 -race does 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.

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.go in the standard library) explains exactly when a plain map plus a Mutex beats 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.

What is this module's central goal?
Why doesn't a clean go vet pass mean a goroutine-spawning function is safe?
What is the minimum possible value runtime.NumGoroutine() prints here, and why not zero?
func main() {
    fmt.Println("goroutines:", runtime.NumGoroutine())
}
According to this lesson, which of the following does the race detector (-race) NOT guarantee?