runtime is the machinery underneath every Go program: the scheduler, the garbage collector, the stack. Most of it you never call, and a few functions are worth knowing.
Examples
Four facts about the binary and the host, available without importing anything else.
fmt.Println(runtime.GOOS, runtime.GOARCH)
fmt.Println("cpus:", runtime.NumCPU())
fmt.Println(runtime.Version())linux amd64
cpus: 8
go1.26.1The count includes the goroutine doing the asking. Watching it over time is the cheapest leak detector there is.
fmt.Println("before:", runtime.NumGoroutine())
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() { defer wg.Done() }()
}
wg.Wait()
fmt.Println("after:", runtime.NumGoroutine())before: 1
after: 1ReadMemStats fills a struct you own. Alloc is what is live now; TotalAlloc only ever climbs.
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("live %d KB, total %d KB, %d collections\n",
m.Alloc/1024, m.TotalAlloc/1024, m.NumGC)live 168 KB, total 168 KB, 0 collectionsOne frame up is your caller, which is what a logger wants to report.
func logLine(msg string) {
_, file, line, ok := runtime.Caller(1)
if !ok {
file, line = "?", 0
}
fmt.Printf("%s:%d %s\n", filepath.Base(file), line, msg)
}
logLine("started")main.go:18 started| Function | Description |
|---|---|
GOOS runtime.GOOS == "linux" | The operating system the binary was built for: linux, darwin, windows. |
| |
GOARCH runtime.GOARCH // "amd64" | The architecture the binary was built for: amd64, arm64. |
NumCPU() int runtime.NumCPU() // 8 | How many hardware threads the machine reports. |
| |
Version() string runtime.Version() // "go1.26.1" | The Go version the binary was built with. |
NumGoroutine() int | How many goroutines exist right now. |
A goroutine that blocks forever is never collected, so a count that only climbs is a leak. Sampling it on a ticker costs nothing and catches the class of bug that otherwise shows up as a server growing quietly for a week. | |
Gosched() runtime.Gosched() | Hands the processor to another goroutine and comes back. |
| |
GOMAXPROCS(n int) int runtime.GOMAXPROCS(0) // read without setting | Sets how many goroutines may run at once, and returns the previous value. |
| |
ReadMemStats(m *MemStats) runtime.ReadMemStats(&m) | Fills a MemStats with the current heap and GC numbers. |
| |
MemStats | The heap numbers: Alloc is live bytes, TotalAlloc is everything ever allocated, Sys is what was taken from the OS, NumGC counts collections. |
GC() runtime.GC() | Runs a garbage collection and waits for it. |
| |
SetFinalizer(obj any, finalizer any) runtime.SetFinalizer(f, cleanup) | Runs a function when the object becomes unreachable. |
| |
Caller(skip int) (pc uintptr, file string, line int, ok bool) _, file, line, _ := runtime.Caller(1) | The file and line of the caller, skip frames up. |
| |
FuncForPC(pc uintptr) *Func runtime.FuncForPC(pc).Name() | The function containing a program counter, for its name. |
Stack(buf []byte, all bool) int runtime.Stack(buf, true) | Writes a stack trace into buf, optionally for every goroutine. |
| |
Related
The scheduler and the collector are tuned for the general case and are usually better at their jobs than a guess made from outside. Gosched, GC and SetFinalizer in particular tend to appear in code that wanted a channel, a benchmark, or a defer.
The parts that earn their place every day are the read-only ones: which platform this is, how many goroutines exist, how much memory is live, and where a call came from.