Go Internals - Essentials
Introduction
After the Data Structures with Golang series, the next step is to look under the hood. Not at how to use slices, maps, or goroutines, but at how the language and runtime actually implement them.
This article is Essentials: a map of the territory. Each following article does a deep dive into one layer of Go internals — Scheduler, Memory, Allocator, Garbage Collector, Built-in Types, Interfaces, Concurrency, and Compiler. We will read real source, run small experiments, and connect behavior you already know from application code to the machinery behind it.
The entire series targets Go 1.23. Internals evolve between releases; when something shifts in a later Go version, we will call it out in the relevant article.
What "Go internals" actually covers
When people say "Go internals," they usually mean two cooperating systems:
- The toolchain (
cmd/compile,cmd/link,cmd/asm) — turns your.gofiles into a binary. - The runtime (
runtime) — linked into every Go program and responsible for scheduling, memory, GC, stacks, and the implementation of core language features.
Your main function is not the real entry point. The linker sets up the runtime first; only then does your code run.
flowchart LR
subgraph toolchain["Toolchain"]
A[".go sources"] --> B["compile"]
B --> C["link"]
end
C --> D["executable"]
D --> E["runtime init"]
E --> F["main.main()"]
Most articles focus on the runtime side of that diagram: what happens after the binary starts, and what the compiler had to arrange so the runtime can do its job. Compiler closes the series on the toolchain side — how your source becomes the code and metadata the runtime consumes.
Series roadmap
The runtime is not one big ball of code. It is organized in layers that depend on each other. Later articles assume earlier ones; that is why we split the series vertically instead of dumping everything into a single post.
flowchart TB
subgraph sched["Scheduling"]
GMP["G / M / P scheduler"]
end
subgraph mem["Memory"]
STK["Stacks and escape analysis"]
ALLOC["Heap allocator"]
GC["Garbage collector"]
end
subgraph lang["Language machinery"]
BUILTIN["Slices, strings, maps"]
IFACE["Interfaces and dispatch"]
SYNC["Channels and sync primitives"]
end
GMP --> STK
STK --> ALLOC
ALLOC --> GC
GC --> BUILTIN
BUILTIN --> IFACE
GMP --> SYNC
IFACE --> SYNC
subgraph tool["Toolchain"]
COMP["Compiler pipeline"]
end
IFACE --> COMP
SYNC --> COMP
| Article | Layer | What we will go deep on |
|---|---|---|
| Essentials (this article) | Overview | Build pipeline, runtime role, how to read the source |
| Scheduler | Scheduling | Goroutines vs OS threads, G/M/P, work stealing, preemption |
| Memory | Memory model | Stack growth, heap, escape analysis, when values leave the stack |
| Allocator | Heap allocation | Size classes, spans, mcache / mcentral / mheap |
| Garbage Collector | Reclamation | Tri-color marking, write barriers, phases, STW, GOGC |
| Built-in Types | Core types | slice header, string layout, map (hmap) internals |
| Interfaces | Dispatch | iface / eface, itabs, dynamic dispatch cost |
| Concurrency | Sync primitives | Channel buffers, select, mutex and semaphore primitives |
| Compiler | Toolchain | SSA, inlining, bounds-check elimination, what the compiler emits for the runtime |
Built-in Types deliberately bridges back to the data structures series: you already implemented hash tables; we will see what the runtime's map does differently.
From go build to a running program
A minimal program is enough to anchor the rest of the series:
package main
import "fmt"
func main() {
fmt.Println("hello")
}
go build -o hello .
./hello
Under the hood, go build invokes the compiler and linker. The compiler emits machine code and metadata the runtime needs: stack maps, type descriptors, relocation info. The linker produces an executable with the runtime package linked in.
At process start, the runtime initializes itself (scheduler, memory arenas, GC, signal handlers) before main.main runs. If you have never stepped through that path, it is worth opening src/runtime/proc.go and searching for runtime.main once; Scheduler will refer back to it.
How to follow along
You do not need to fork the Go repo, but you do need the standard library and runtime source for Go 1.23 — the same version as your toolchain.
go version # should report go1.23.x
go env GOROOT
Runtime code lives at $GOROOT/src/runtime/. Compiler code is under $GOROOT/src/cmd/compile/. Browsing on go.dev/src is the same tree; select the go1.23 tag to match this series.
Useful habits for the deep dives:
- Small reproducers — ten lines of Go that exhibit one behavior (escape, GC pressure, goroutine churn).
- Compiler feedback —
go build -gcflags="-m"for escape analysis (Memory). - Runtime tracing —
GODEBUG=gctrace=1,...for GC (Garbage Collector), scheduler traces where relevant (Scheduler). - Debugger — Delve to inspect stack frames and goroutine state while reading scheduler concepts.
We will not treat unsafe as a goal. It appears only where it clarifies how the runtime represents values in memory.
What you should already know
This series is deeper than the data structures articles, but it is not a Go tutorial. Comfortable reading of:
- pointers, structs, and interfaces at the language level
- goroutines and channels in everyday code
- basic complexity intuition (
O(1)vsO(n))
is enough. If you finished the data structures series, you are in the right place.
How this differs from the data structures series
| Data structures series | Go internals series | |
|---|---|---|
| Goal | Implement and compare structures | Explain runtime and language machinery |
| Code focus | Your own Container implementations |
Small programs + reading runtime source |
| Depth | Algorithm + API trade-offs | Memory layout, syscalls, scheduler invariants |
| Split | One structure category per article | One internals layer per article |
Same author voice, same preference for concrete examples — but the "artifact" is understanding, not a reusable library.
Conclusion
Essentials sets the route: toolchain builds the binary, runtime owns execution and memory, and each upcoming article isolates one layer for a proper deep dive.
Next up is Scheduler — how goroutines multiplex onto OS threads, what G, M, and P mean, and why your concurrent Go code behaves the way it does under load.