Runtime Basics
10 examples to get you started with Runtime & GC - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir rtdemo && cd rtdemo && go mod init example.com/rtdemo. - Save snippets as
main.go(or separate files in one package) and run withgo run .. - For GC trace output, run with
GODEBUG=gctrace=1 go run .(verbose; use on small programs).
Basic Examples
1. Read MemStats
runtime.ReadMemStats snapshots allocator and GC counters.
package main
import (
"fmt"
"runtime"
)
func main() {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("heap alloc: %d KiB\n", ms.HeapAlloc/1024)
fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
}HeapAllocis bytes currently allocated on the heap (reachable objects).ReadMemStatsstops the world briefly - do not call every request in hot paths.- Pair with Prometheus
go_memstats_*exporters in production instead of polling in handlers.
Related: The Go Runtime: Scheduler, GC, and Memory - runtime mental model
2. NumGoroutine
Count live goroutines for leak triage.
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
fmt.Println("start:", runtime.NumGoroutine())
go func() {
time.Sleep(200 * time.Millisecond)
}()
time.Sleep(10 * time.Millisecond)
fmt.Println("with worker:", runtime.NumGoroutine())
}- Includes the main goroutine and any runtime background workers.
- A rising count under steady load often signals forgotten
WaitGroupwaits or blocked channel sends. - Export as a gauge and compare to request rate, not an absolute threshold alone.
Related: Goroutine Stacks & Stack Growth - per-goroutine stack cost
3. GOMAXPROCS and NumCPU
See how many Ps the scheduler uses.
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("NumCPU:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
}GOMAXPROCS(0)reads the current setting without changing it.- In cgroups-aware Go builds, the runtime may align Ps with container CPU quota automatically.
- Set explicitly only when you have measured a mismatch between quota and observed parallelism.
Related: Goroutines: Creation Cost & Scheduling Overview - GPM model
4. Manual GC for diagnostics
Force a collection to measure live heap after cleanup.
package main
import (
"fmt"
"runtime"
)
func main() {
data := make([]byte, 10<<20) // 10 MiB
_ = data
runtime.GC()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("after GC heap: %d MiB\n", ms.HeapAlloc/(1<<20))
}runtime.GC()runs a full cycle and returns when complete.- Useful in benchmarks and one-off diagnostics, not per-request production paths.
- Live references keep
HeapAllochigh even immediately after GC.
Related: Garbage Collector: Tri-Color Mark-Sweep & Pacing - what a cycle does
5. GC pause totals
Inspect cumulative STW pause time.
package main
import (
"fmt"
"runtime"
)
func main() {
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("GC cycles: %d\n", ms.NumGC)
fmt.Printf("pause total: %d ms\n", ms.PauseTotalNs/1e6)
if ms.NumGC > 0 {
last := ms.PauseNs[(ms.NumGC+255)%256]
fmt.Printf("last pause: %d µs\n", last/1e3)
}
}PauseNsis a circular buffer of recent pause lengths.- Tail latency SLOs care about individual pauses, not only cumulative totals.
- Compare before/after
GOGCorGOMEMLIMITchanges under load.
Related: GOGC, GOMEMLIMIT & Finalizers - tuning knobs
6. Allocator rate with Mallocs
See how many objects each cycle allocates.
package main
import (
"fmt"
"runtime"
)
func allocN(n int) {
for i := 0; i < n; i++ {
_ = make([]byte, 1024)
}
}
func main() {
var before runtime.MemStats
runtime.ReadMemStats(&before)
allocN(1000)
var after runtime.MemStats
runtime.ReadMemStats(&after)
fmt.Println("mallocs delta:", after.Mallocs-before.Mallocs)
}MallocsandFreescounters increase monotonically for the process lifetime.- High alloc rate drives GC CPU even when live heap stays small.
go test -benchwithbenchmemreports allocations per operation.
Related: Escape Analysis & Stack vs Heap Allocation - reducing heap allocs
7. debug.FreeOSMemory
Return unused spans to the OS after a spike.
package main
import (
"fmt"
"runtime"
"runtime/debug"
)
func main() {
_ = make([]byte, 50<<20)
runtime.GC()
debug.FreeOSMemory()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("heap idle: %d MiB\n", ms.HeapIdle/(1<<20))
}- The runtime holds freed spans in pools;
FreeOSMemoryencourages returning them to the OS. - Useful after rare large-batch jobs in memory-capped pods.
- Does not replace fixing leaks or oversized caches.
Related: Memory Leaks in Go: Common Causes - leak patterns
Intermediate Examples
8. GODEBUG gctrace
Print one line per GC cycle to stderr.
package main
func main() {
for i := 0; i < 1_000_000; i++ {
_ = make([]byte, 256)
}
}- Run:
GODEBUG=gctrace=1 go run . - Each line shows wall time, GC number, heap size, and pause breakdown.
- Use during load tests to see if pacing matches expectations before changing
GOGC.
Related: Runtime & GC Best Practices - production tuning workflow
9. runtime/metrics package
Read structured GC and memory metrics (Go 1.16+).
package main
import (
"fmt"
"runtime/metrics"
)
func main() {
samples := []metrics.Sample{
{Name: "/gc/heap/live:bytes"},
{Name: "/sched/goroutines:goroutines"},
}
metrics.Read(samples)
fmt.Println("live heap:", samples[0].Value.Uint64())
fmt.Println("goroutines:", samples[1].Value.Uint64())
}- Metric names are stable for dashboards; prefer them over hand-parsing
MemStatsin exporters. - See
go doc runtime/metricsfor the full catalog. /gc/cycles/total:gc-cyclespairs well with CPU profiles during GC regressions.
Related: CPU & Heap Profiling with pprof - profile under load
10. Compare stack vs heap footprint
Correlate StackInuse with goroutine fan-out.
package main
import (
"fmt"
"runtime"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10_000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
var buf [4096]byte // grows stack if needed
_ = buf
}()
}
wg.Wait()
var ms runtime.MemStats
runtime.ReadMemStats(&ms)
fmt.Printf("stack inuse: %d MiB, goroutines: %d\n",
ms.StackInuse/(1<<20), runtime.NumGoroutine())
}StackInuseis aggregate stack memory across goroutines.- Large fan-out plus deep stacks can dominate RSS without large heap objects.
- Cap concurrency with worker pools when
StackInusetracks request count linearly.
Related: Goroutine Stacks & Stack Growth - growth and copy rules
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).