runtime Package Deep — Junior¶
1. What is the runtime package?¶
There are two things in Go that both go by the name "runtime", and people mix them up constantly:
- The runtime (lowercase, conceptual) — the scheduler, garbage collector, memory allocator, and goroutine machinery baked into every Go binary. This is what subtopics 01–04 of this section covered. You can't
importit. You don't call it directly. It just runs. - The
runtimepackage (the importable one) — a thin, official Go API that exposes a slice of those internals to your code. You writeimport "runtime"and call functions likeruntime.NumGoroutine()orruntime.GC().
This subtopic is about #2 — the user-facing runtime package. It is the only sanctioned door into the runtime. Everything else (the g, m, p structs, mallocgc, schedule) is unexported and not part of the Go 1.x compatibility promise.
Think of it as the dashboard of a car. The engine (the actual runtime) does the work. The dashboard (the
runtimepackage) shows you a handful of gauges and gives you a few buttons — speedometer, fuel level, hazard lights. You don't drive with the dashboard, but you'd be flying blind without it.
2. runtime vs runtime. — naming clarified¶
When this document or any Go documentation says:
- "the runtime" — the conceptual subsystem (scheduler + GC + allocator + ...).
runtimepackage orruntime.X— the importable Go package at$GOROOT/src/runtimewhose exported identifiers (uppercase) you can use.
The same directory holds both: $GOROOT/src/runtime/*.go. Lowercase identifiers in those files (schedule, gopark, mallocgc) are internal. Uppercase ones (NumGoroutine, GC, ReadMemStats) are the public API.
So runtime.GC() (uppercase G, exported) lives in runtime/mgc.go and is a thin wrapper around the unexported gcStart. Reading the unexported half is a separate skill (subtopic 01). Calling the exported half is what most programmers will ever do, and that's the focus here.
3. Prerequisites¶
- You can write a small Go program with goroutines and channels.
- You've at least heard of garbage collection.
- You're comfortable opening
pkg.go.dev/runtimeand skimming the function list.
You do not need to have read runtime source. This level is about using the package, not understanding its guts.
4. Glossary¶
| Term | Meaning |
|---|---|
runtime package | The importable Go package whose exported API exposes runtime internals to user code |
runtime/debug | Subpackage for debugging: stack traces, GC tuning, build info |
runtime/pprof | Subpackage for CPU/heap/goroutine profiling |
runtime/trace | Subpackage for execution traces (scheduler events, GC, syscalls) |
runtime/metrics | Subpackage (Go 1.16+) — the modern, stable, structured way to read runtime stats |
runtime/race | Subpackage that ships with the race detector (-race flag) |
MemStats | A struct populated by runtime.ReadMemStats containing heap, GC, and allocator counters |
GOMAXPROCS | The number of OS threads simultaneously executing Go code |
Gosched | Voluntarily yield the CPU to let other goroutines run |
Caller / Stack | Functions to inspect the current call stack for debugging or logging |
5. The subpackages — a quick map¶
The exported runtime package has a small surface (~50 functions). But it's the root of a family of subpackages, each focused on a particular task:
| Path | Purpose | Typical use |
|---|---|---|
runtime | Core API: goroutines, GC, env info | NumGoroutine, GC, GOMAXPROCS |
runtime/debug | Stack dumps, GC tuning, build info | debug.Stack(), debug.SetGCPercent, debug.ReadBuildInfo |
runtime/pprof | Profilers (CPU, heap, block, mutex) | pprof.StartCPUProfile, pprof.WriteHeapProfile |
runtime/trace | Execution traces for go tool trace | trace.Start, trace.Stop |
runtime/metrics | Stable, named, structured runtime metrics | metrics.Read, metrics.All |
runtime/race | Linked in automatically with go test -race | (not directly imported) |
runtime/metrics is the modern replacement for many MemStats use cases. New code reading runtime stats over time should prefer it. We'll see both styles below.
6. Functions you'll actually use early on¶
These are the half-dozen calls a junior Go programmer encounters first. Memorize them — they show up in tutorials, blog posts, and every "diagnose a weird Go problem" guide.
import "runtime"
// How many goroutines exist right now (across the whole program).
n := runtime.NumGoroutine()
// How many logical CPUs the OS reports.
ncpu := runtime.NumCPU()
// How many CPUs Go is *allowed* to use simultaneously.
// Passing 0 means "tell me without changing".
gmp := runtime.GOMAXPROCS(0)
// Yield to the scheduler — lets other goroutines run.
// Almost never needed in well-written code; useful in tight CPU loops.
runtime.Gosched()
// Force a full garbage collection cycle. Blocks until done.
// Useful in tests/benchmarks; rarely a good idea in production code.
runtime.GC()
// Snapshot of memory stats into a caller-provided struct.
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
Each of these is a one-line API hiding a substantial chunk of internal work. NumGoroutine reads the global sched.gcount. GC calls gcStart(gcTrigger{kind: gcTriggerCycle}). Gosched calls mcall(gosched_m) to swap to the g0 stack and re-enter the scheduler.
7. Environment introspection¶
The runtime package also exposes constants and one function describing the build environment. These are evaluated at compile time (the constants) or read from a runtime variable (the function):
fmt.Println("Go version: ", runtime.Version()) // e.g. "go1.22.3"
fmt.Println("OS: ", runtime.GOOS) // "linux", "darwin", "windows"
fmt.Println("Arch: ", runtime.GOARCH) // "amd64", "arm64"
fmt.Println("Compiler: ", runtime.Compiler) // "gc" or "gccgo"
runtime.GOOS and runtime.GOARCH are typed string constants. They're useful for log lines ("starting on linux/amd64") and for the occasional platform check inside test helpers. They are not the right way to do cross-platform code — build tags (//go:build linux) and _linux.go file suffixes are.
8. Debugging helpers: Caller and Stack¶
Two functions every Go programmer eventually meets:
// runtime.Caller(skip int) returns info about the caller `skip` frames up.
// skip=0 is the call to Caller itself; skip=1 is the function that called you.
pc, file, line, ok := runtime.Caller(1)
if ok {
fmt.Printf("called from %s:%d\n", file, line)
_ = pc // program counter, useful with runtime.FuncForPC
}
// runtime.Stack(buf, all) writes a stack trace into buf.
// If all=true, dumps every goroutine — extremely useful when diagnosing a hang.
buf := make([]byte, 1<<16)
n := runtime.Stack(buf, true)
fmt.Println(string(buf[:n]))
Caller is how logging libraries figure out which file/line invoked log.Info(...). Stack with all=true is the same dump you get when a Go program crashes with an unrecovered panic — useful for a "dump everything before we exit" hook.
For cleaner stack traces in a structured format, prefer runtime.Callers + runtime.CallersFrames (returns a Frames iterator over pc values). runtime/debug.Stack() is a one-liner that produces the trace of just the current goroutine without you sizing a buffer.
9. A small intro program¶
A working example that touches six runtime calls. Save as runtime_demo.go, then go run runtime_demo.go.
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
fmt.Printf("Go %s on %s/%s, %d CPUs, GOMAXPROCS=%d\n",
runtime.Version(),
runtime.GOOS,
runtime.GOARCH,
runtime.NumCPU(),
runtime.GOMAXPROCS(0),
)
fmt.Println("Goroutines at start:", runtime.NumGoroutine())
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Tight loop that yields voluntarily; without Gosched this
// could starve other goroutines on a single-P configuration.
for j := 0; j < 1000; j++ {
if j%100 == 0 {
runtime.Gosched()
}
}
}(i)
}
fmt.Println("Goroutines mid-flight:", runtime.NumGoroutine())
wg.Wait()
runtime.GC() // make MemStats reflect a clean state
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("HeapAlloc=%d bytes, NumGC=%d, PauseTotalNs=%d\n",
ms.HeapAlloc, ms.NumGC, ms.PauseTotalNs)
fmt.Println("Goroutines at end:", runtime.NumGoroutine())
}
This program uses Version, GOOS, GOARCH, NumCPU, GOMAXPROCS, NumGoroutine, Gosched, GC, and ReadMemStats. Almost the entire junior-level surface in 30 lines.
10. A quick tour of MemStats¶
runtime.MemStats has ~25 fields. You don't need all of them. The ones that matter early:
| Field | Meaning |
|---|---|
Alloc / HeapAlloc | Bytes of heap memory currently in live (non-freed) objects |
TotalAlloc | Cumulative bytes allocated over the program's lifetime (never decreases) |
Sys | Total bytes obtained from the OS (heap + stacks + runtime overhead) |
HeapObjects | Number of live heap objects |
NumGC | How many GC cycles have run |
PauseTotalNs | Cumulative nanoseconds the program was stopped for GC |
PauseNs[(NumGC+255)%256] | Most recent GC pause |
NextGC | Heap size that will trigger the next GC |
GCCPUFraction | Fraction of total CPU time spent in GC (0.0–1.0) |
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("heap live: %d KB\n", ms.HeapAlloc/1024)
fmt.Printf("total ever: %d KB\n", ms.TotalAlloc/1024)
fmt.Printf("GC cycles: %d\n", ms.NumGC)
fmt.Printf("last pause: %v ns\n", ms.PauseNs[(ms.NumGC+255)%256])
ReadMemStats is not free — it stops the world briefly. Don't call it in a hot path. Once a second from a metrics goroutine is fine; once per request is not.
For long-lived monitoring, the modern alternative is runtime/metrics:
import "runtime/metrics"
samples := []metrics.Sample{
{Name: "/memory/classes/heap/objects:bytes"},
{Name: "/sched/goroutines:goroutines"},
}
metrics.Read(samples)
fmt.Println(samples[0].Value.Uint64(), samples[1].Value.Uint64())
runtime/metrics has a stable, documented naming scheme and doesn't carry the legacy baggage of MemStats.
11. Common confusions¶
- "Should I sprinkle
runtime.GC()in my code?" Almost always no. The GC runs on its own schedule, tuned byGOGCand the pacer. Forcing collections wastes CPU and rarely helps. Exceptions: deterministic benchmarks, tests asserting "no leak after teardown", anddebug.FreeOSMemory()right before a long sleep on a memory-tight system. - "Does
runtime.GC()free memory back to the OS?" No. It runs a mark-and-sweep cycle, marking freed objects as reusable by the Go heap. Returning pages to the OS is a separate concern — handled lazily by the scavenger, or forcibly bydebug.FreeOSMemory(). - "Does
runtime.Gosched()sleep?" No. It puts the current goroutine back on the run queue and re-enters the scheduler. If nothing else is runnable, the same goroutine resumes immediately. It is nottime.Sleep(0). - "Is
runtime.NumGoroutine()exact?" It's a live count fromsched.gcount, atomically updated. It includes the goroutine asking. It does not tell you which goroutines exist — for that, useruntime.Stack(buf, true). - "
runtime.GOMAXPROCS(8)makes my program faster"? Maybe. It caps the number of OS threads running Go code concurrently. Default since Go 1.5 isNumCPU(). Setting it higher thanNumCPU()rarely helps; setting it lower can help under heavy oversubscription. Don't tune it without measuring. - "Why use
runtimein production at all?" Reasonable cases: exposing/debug/pprofendpoints, periodicruntime/metricssampling for Prometheus, aSIGQUIThandler that dumpsruntime.Stack(buf, true)for postmortem. Outside of diagnostics, you mostly shouldn't reach for it.
12. Summary¶
The runtime package is the official, importable interface to the Go runtime — the dashboard on top of the engine. Its core gives you environment info (Version, GOOS, GOARCH, NumCPU), goroutine and scheduler controls (NumGoroutine, GOMAXPROCS, Gosched), garbage collector controls (GC, ReadMemStats), and debugging helpers (Caller, Stack). Its subpackages — debug, pprof, trace, metrics, race — handle stack traces, profiling, execution traces, structured metrics, and the race detector.
At this level the goal is to know the menu: which function answers which question. You don't need to call runtime.GC() in real code; you do need to recognize it in someone else's. You should be able to write a 30-line program that prints Go version, goroutine count, heap size, and GC cycles — and to read pkg.go.dev/runtime without feeling lost.
Further reading¶
runtimepackage docs:https://pkg.go.dev/runtimeruntime/debugdocs:https://pkg.go.dev/runtime/debugruntime/metricsdocs:https://pkg.go.dev/runtime/metrics— the modern stats APIruntime/pprofdocs:https://pkg.go.dev/runtime/pprof- "Diagnostics" — official Go blog/doc:
https://go.dev/doc/diagnostics— tour of the runtime-adjacent tooling - "Go's memory statistics" — Dave Cheney — readable walk-through of
MemStatsfields - Source:
$GOROOT/src/runtime/debug.go,$GOROOT/src/runtime/mstats.go,$GOROOT/src/runtime/proc.go