Skip to Content
Course content

205: Runtime Statistics and Memory Stats

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Listen, in a perfect world, you'd just let the Go garbage collector (GC) do its thing and never think about memory again. But as your services grow, you'll eventually hit a wall where you need to know exactly what's happening under the hood. Are you leaking memory? Is your heap growing faster than you expected? That's where the runtime package comes in.

To see this in action, I'm going to build a simple "Session Cache." It's basically a map that stores large chunks of dummy data. I want to track how much memory this cache is consuming in real-time without relying on external OS tools like top or htop.

Setting up a memory-hungry store

First, let's create a store that actually puts some pressure on the heap. I'll use a map of byte slices to simulate session data. If we just used a few integers, we wouldn't see the memory stats move enough to be interesting.

package main

import (
	"fmt"
	"runtime"
)

type SessionStore map[int][]byte

func (s SessionStore) AddSession(id int) {
	// Allocate 1MB of dummy data per session
	s[id] = make([]byte, 1024*1024)
}

func main() {
	store := make(SessionStore)
	for i := 0; i < 10; i++ {
		store.AddSession(i)
	}
	// We'll add the stats logic here
}

Peeking into the runtime

Now, I want to see how much memory we've used. The runtime.ReadMemStats function is our primary tool here. It requires a pointer to a runtime.MemStats struct, which it then populates with a snapshot of the current memory state.

I'll wrap this in a helper function so we can call it multiple times throughout the program's lifecycle.

func printMemUsage() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	fmt.Printf("Alloc = %v, TotalAlloc = %v, Sys = %v, NumGC = %v\n", m.Alloc, m.TotalAlloc, m.Sys, m.NumGC)
}

Fixing my "Wall of Numbers" mistake

When I first ran this, I realized I made a classic mistake: I forgot that runtime.MemStats reports everything in bytes. When I printed the output, I got something like Alloc = 10485760. While correct, it's a pain to read when you're debugging a production issue at 2 AM.

I need to convert these values into Megabytes to make them human-readable. I'll update the helper function to do the math for me. I'm also going to add a call to this function before and after we populate the store so we can see the delta.

func printMemUsage() {
	var m runtime.MemStats
	runtime.ReadMemStats(&m)
	// Convert bytes to MiB for sanity
	fmt.Printf("Alloc: %v MiB\tTotalAlloc: %v MiB\tSys: %v MiB\tNumGC: %v\n", 
		m.Alloc/1024/1024, 
		m.TotalAlloc/1024/1024, 
		m.Sys/1024/1024, 
		m.NumGC)
}

func main() {
	fmt.Println("Initial state:")
	printMemUsage()

	store := make(SessionStore)
	for i := 0; i < 10; i++ {
		store.AddSession(i)
	}

	fmt.Println("After adding 10 sessions:")
	printMemUsage()
}

Watching the Garbage Collector work

The most interesting part of MemStats is seeing the difference between Alloc (heap memory currently in use) and TotalAlloc (cumulative bytes allocated since the program started). To really see the GC in action, I'll clear the store and manually trigger a GC collection using runtime.GC().

Here is the final flow I'm using to verify that the memory is actually being freed:

func main() {
	store := make(SessionStore)
	for i := 0; i < 20; i++ {
		store.AddSession(i)
	}
	fmt.Print("Full store: ")
	printMemUsage()

	// Nil out the store to make it eligible for GC
	store = nil 
	
	// Force a GC cycle now so we don't have to wait for the runtime to decide
	runtime.GC()

	fmt.Print("After GC:    ")
	printMemUsage()
}

When you run this, you'll notice Alloc drops significantly after the runtime.GC() call, but TotalAlloc stays high. That's because TotalAlloc is a monotonically increasing counter—it never goes down. If you see Alloc climbing steadily without ever dropping, you've found your memory leak.




📋 Practical Task

Build a Heap-Growth Monitor

Create a program that simulates a memory leak by continuously appending large strings to a global slice. Your program should:

  • Implement a monitorMemory() function that prints the current Alloc and HeapObjects from runtime.MemStats every 500 milliseconds using a time.Ticker.
  • In a separate goroutine, implement a loop that adds a 1MB string to a slice every 200 milliseconds.
  • Run the program for 5 seconds, then stop the loop and call runtime.GC() to see if the memory is reclaimed.
  • Print the final memory stats to verify if the "leak" was cleared after the slice was cleared.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.