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
63: Profiling Go Applications with pprof
You've probably hit a point in your projects where the code works, but it feels... sluggish. Maybe your memory usage is climbing steadily, or a specific request is taking 200ms when it should take 20ms. You could pepper your code with time.Since() calls, but that's a blunt instrument. That's why we use pprof.
It's the gold standard for profiling in Go. Instead of guessing, pprof lets you see exactly which line of code is eating your CPU cycles or hogging the heap. I'm going to show you how to set it up using a scenario I've run into a dozen times: a "data processor" that's accidentally doing way too much work.
Building a slow log processor
Let's start with a piece of code that's intentionally inefficient. We're simulating a function that processes a massive slice of log entries and builds a summary string. I'm using fmt.Sprintf inside a tight loop, which is a classic way to kill performance in Go due to constant allocations.
package main
import (
"fmt"
"net/http"
"runtime"
"time"
)
func processLogs(logs []string) string {
var result string
for _, log := range logs {
// This is the "hidden" performance killer
result += fmt.Sprintf("Processed: %s\n", log)
}
return result
}
func main() {
// Start a background server for pprof
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
fmt.Println("Server started on :6060. Processing logs...")
// Create a large dummy dataset
logs := make([]string, 100000)
for i := 0; i < 100000; i++ {
logs[i] = fmt.Sprintf("log entry number %d", i)
}
for {
processLogs(logs)
time.Sleep(time.Millisecond * 100)
}
}
The "Where is the endpoint?" mistake
I'll be honest—the first time I used pprof, I wrote the code above, ran it, and then tried to visit http://localhost:6060/debug/pprof in my browser. I got a 404. I spent ten minutes questioning if my firewall was blocking the port or if ListenAndServe had failed silently.
The problem? I forgot the "blank import." In Go, the net/http/pprof package registers its handlers with the http.DefaultServeMux, but it only does that if the package is imported. Because I wasn't calling any functions from that package directly, I needed to import it for its side effects.
To fix it, I added this to my imports:
import _ "net/http/pprof"
Now, when the program starts, that blank import triggers the init() function inside the pprof package, which attaches the /debug/pprof handlers to the default server. Now the endpoint actually exists.
Capturing the CPU profile
With the server running, we don't just look at the browser. The real power is in the go tool pprof command-line utility. I want to see where the CPU is spending its time, so I'll run this in my terminal while the app is humming along:
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
This tells Go to sample the application for 30 seconds. Once it finishes, you'll be dropped into an interactive prompt. You can type top to see the heaviest functions, but I prefer the visual representation. If you have Graphviz installed, you can run web, or just start the tool with the -http flag to get a beautiful browser-based UI:
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
Identifying the culprit in the graph
When you open the web UI, look at the "Graph" view. You'll see boxes connected by arrows. The larger the box and the thicker the arrow, the more resources are being consumed. In our log processor, you'll see a massive node for runtime.mallocgc and fmt.Sprintf.
The graph makes it obvious: we aren't spending time "processing logs"; we're spending almost all our time allocating memory for new strings. Every time I used result += ..., Go had to create a brand new string in memory and copy the old content into it. That's an O(n²) operation that kills performance as the input grows.
To fix this, I'd replace the string concatenation with a strings.Builder. If you ran the profiler again after that change, you'd see the mallocgc node shrink significantly, and the CPU usage would plummet.
📋 Practical Task
Exercise: Hunting the Heap in a Mock Cache
You are given a program that implements a simple in-memory cache. However, the developer forgot to implement a cleanup mechanism, and the memory usage grows indefinitely (a classic memory leak). Your goal is to use pprof to prove where the memory is being held.
Requirements:
- Create a program that starts an HTTP server on port 6060 with
net/http/pprofenabled. - Implement a global
map[string][]bytethat acts as a cache. - Create a goroutine that continuously adds large random byte slices (e.g., 1MB each) to this map using unique keys.
- Run the application and use
go tool pprofto capture a heap profile (not a CPU profile). - Use the
topcommand or the web UI to identify the specific line of code responsible for the most memory allocations. - Fix the leak: Implement a simple "TTL" (Time To Live) or a maximum size limit for the map to stabilize memory usage, then verify the fix by taking a second heap profile.
There are no comments for now.