Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
205: Runtime Statistics and Memory Stats
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 currentAllocandHeapObjectsfromruntime.MemStatsevery 500 milliseconds using atime.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.
There are no comments for now.