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
221: Profiling CPU and Memory with pprof Deep Dive
I've seen this happen to almost every developer I've mentored: they run a memory profile, see a function at the top of the list with a massive amount of memory allocated, and spend three days trying to "optimize" that function. They think they've found the leak. But after all that work, the memory usage stays exactly the same. Why? Because they were looking at alloc_space instead of inuse_space.
Thinking "Allocations" Equal "Leaks" vs. Tracking what is actually staying in RAM
Here is the concrete reason that approach fails: pprof tracks two very different things. alloc_space tells you every single byte that has passed through that line of code since the program started. If you have a loop that allocates a 1KB buffer, uses it, and lets it be garbage collected, a million iterations will show 1GB of alloc_space. But your inuse_space—the memory actually occupying your RAM right now—will be practically zero.
func processData(input []byte) {
// If this is called 1 million times, pprof's 'alloc_space'
// will show massive numbers here, even though the memory
// is freed immediately after the function returns.
tempBuf := make([]byte, 1024)
copy(tempBuf, input)
doSomething(tempBuf)
}
If you're hunting a leak, you don't care how much memory was allocated over the lifetime of the app; you care what's still there. When you run go tool pprof, you need to explicitly switch the sample type. Use sample_index = inuse_space to see what's actually bloating your heap. If a function is high in alloc_space but low in inuse_space, it's not a leak—it's just "churn," which might be a performance issue (GC pressure), but it's not why your server is hitting OOM (Out of Memory) kills.
Assuming the CPU Profile reveals "Slow Code" vs. Identifying where the CPU is actually working
There's another trap with CPU profiling. You'll see a function taking 40% of the CPU and assume that function is "slow." But CPU profiling is a sampling mechanism. It tells you where the CPU was executing instructions when the timer went off. It does not tell you how long a function took to complete (wall-clock time).
I once spent a whole afternoon trying to optimize a JSON parsing function because it topped the CPU profile. In reality, the parser was fast; it was just being called ten thousand times more than it should have been because of a bug in the caching layer. The "slow" part wasn't the logic inside the function, but the frequency of its invocation. Always ask yourself: "Is this function inefficient, or is it just being called too often?"
Moving from the Global View to the Line-of-Code View
The graph view (the one that looks like a bunch of boxes and arrows) is great for a bird's-eye view, but it's where most people stop. To actually fix a bug, you need the list command. Once you've identified a suspicious function using top, use list inside the pprof interactive shell.
This is the "aha!" moment. list will annotate your actual source code with the exact number of bytes or CPU milliseconds spent on each specific line. I've found that this often reveals a surprising culprit—like a fmt.Sprintf call inside a tight loop that's eating 20% of your CPU, which you would never have guessed just by looking at the function name in a graph.
To get this data, remember to wrap your server in the pprof handler:
import _ "net/http/pprof"
import "net/http"
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// your application logic here
}
Then, while the app is running under load, hit it with: go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 for CPU, or .../heap for memory.
📋 Practical Task
Exercise: Fixing the Persistent Buffer Leak
You have been handed a high-throughput event processor. The developer before you implemented a "small optimization" to avoid allocations by slicing a large pre-allocated buffer. However, the application's memory usage grows linearly and never drops, even after events are processed.
The Scenario:
The program reads a 1MB "Event Batch" from a mock network source. It then extracts a small "Event ID" (about 16 bytes) from that batch and stores it in a global processedIDs slice for deduplication.
var processedIDs [][]byte
func handleBatch(batch []byte) {
// Extracting the ID from the first 16 bytes of the batch
id := batch[:16]
processedIDs = append(processedIDs, id)
}
Your Task:
- Run the provided code (which simulates thousands of batches) and use
go tool pprofto analyze the heap. - Identify why
inuse_spaceis skyrocketing even though you are only storing 16 bytes per ID. (Hint: Look at how Go slices reference the underlying array). - Fix the leak by ensuring the small ID does not keep the entire 1MB batch alive in memory.
- Verify the fix by running the profile again and confirming that
inuse_spacenow correlates with the number of IDs, not the size of the original batches.
There are no comments for now.