Dev Skills

Profile it, ship it, and what comes next

Measure before you optimise, push it, and pick the right next thing to build.

Topic 87 of 98 · 5 min to read · 50 min to build
After this topic you can
  • Capture a CPU or memory profile with pprof and find the hot spot.
  • Write an honest benchmark with b.Loop or b.N and compare results with benchstat.
  • Explain why you measure before optimising.

What profiling is

Profiling is measuring where a running program spends its time and memory. It matters because intuition about this is often wrong. The line you are sure is slow may not be, and the cost is somewhere you never looked.

Go ships a profiler in the standard library, called pprof. You expose it on a port, take a sample while the program is under load, and it shows which functions the time went into.

The rule that follows from this: correct first, clear second, fast only when a measurement says so. An optimisation without a profile is a guess, and it often costs readability and buys nothing.

pprof: never guess about performance

cmd/api/main.go
import _ "net/http/pprof" // registers handlers on the DEFAULT mux
// a SEPARATE, localhost-only port — never your public one
go func() {
slog.Info("pprof on localhost:6060")
// Log the error: losing the profiler should not kill the service,
// but you want to know it failed (port in use, for example).
if err := http.ListenAndServe("localhost:6060", nil); err != nil {
slog.Error("pprof server stopped", "err", err)
}
}()
Exposing /debug/pprof publicly leaks your function names and source paths, and lets anyone start CPU profiles and heap dumps that cost you CPU. Bind it to localhost.
terminal
# CPU: where is time going?
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
# memory in use right now
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
# total allocated over time — the allocation churn
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/allocs
# goroutine leak hunting: does this number grow and never come back down?
curl -s 'localhost:6060/debug/pprof/goroutine?debug=1' | head -1
In the web UI: Flame Graph for where time goes, Top for a ranked list of functions, Source for line by line.

What usually costs you, roughly in order

#CauseTypical fix
1a query inside a loop (N+1)a JOIN, or one batched query
2a missing database indexEXPLAIN ANALYZE, then CREATE INDEX
3a pool that is too small, or unlimitedSetMaxOpenConns
4unbounded concurrencya semaphore, or errgroup.SetLimit
5allocations in a hot looppreallocate with make([]T, 0, n)
6everything elserarely worth touching until a profile points at it
Watch out: Do not do these on faith: sync.Pool, unsafe string conversion, replacing a mutex with atomics, hand-rolled lock-free structures. Each needs a benchmark proving it *in your workload*. Without one, you pay in readability and may gain nothing.

Benchmark honestly

bench_test.go
func BenchmarkValidate(b *testing.B) {
req := CreateRequest{Title: "hello", Body: "world"}
b.ReportAllocs()
b.ResetTimer() // exclude the setup above
for i := 0; i < b.N; i++ {
_ = req.Validate()
}
}
terminal
go test -bench=. -benchmem -count=10 ./... > old.txt
# ... make the change ...
go test -bench=. -benchmem -count=10 ./... > new.txt
benchstat old.txt new.txt
go install golang.org/x/perf/cmd/benchstat@latest
A single benchmark run is noisy. -count=10 plus benchstat tells you whether a difference is bigger than the noise.

Ship it

terminal
make check # fmt, vet, race, govulncheck — all clean
git init && git add -A
git commit -m "SnapNotes API"
git remote add origin git@github.com:you/snapnotes.git
git push -u origin main

The habit that makes you senior

before every push
gofmt -l . # silent
go vet ./... # silent
go test -race ./... # green
govulncheck ./... # clean
Then read the diff you are about to push as if someone else wrote it, and cut what it does not need.

What to build next

  • A CLI with flag and subcommands: a different shape of program from a server.
  • A concurrent web crawler with bounded workers, dedup, and a context timeout. It makes you use every concurrency tool from this track at once.
  • A rate limiter as a library, with benchmarks and fuzz tests.
  • A queue worker (Redis, NATS, or SQS) with graceful shutdown and retries.
  • Add caching, metrics, and tracing to SnapNotes.
  • Effective Go: reread it in a month. It means much more once you have written real code.
  • Go Code Review Comments: the Go wiki's list of what reviewers commonly flag.
  • The Go Memory Model: short, and it settles arguments about when one goroutine is guaranteed to see another's writes.
  • The Go Blog: start with pipelines, context, and errors-are-values.
  • The standard library source. net/http, io, errors, and sync are some of the clearest Go you can read, and it is already on your disk.
Worth knowing: You started this track not knowing what a goroutine was. You now have a Go API with authentication, a database, file uploads, tests, and a container that runs it. That is a real portfolio project and a real skill. Good Go code is boring. If a reviewer says "this is clever", that is not the compliment it sounds like.
The short version
  • Profile before optimising. pprof on localhost only, never a public port.
  • The database is where your latency lives. Micro-optimising Go is the last resort.
  • Benchmark with -count=10 and benchstat, or you are reading noise.
  • fmt, vet, race, govulncheck before every push. Forever.
Remember: Correct, then clear, then fast, and only fast when a measurement told you to. An optimisation written without a profile often makes the code harder to read and no faster.
Build this before moving on
Profile the API under a hey or ab load test, find the slowest endpoint, and fix one real thing. Then run make check, push it, and take the rest of the day off.
Then try these in your own editor
  1. 1Load-test with hey -n 2000 -c 50 and capture a 30-second CPU profile while it runs.
  2. 2Find your slowest endpoint from request latency (not the CPU profile) and fix one real thing.
  3. 3Benchmark that fix with -count=10 and benchstat. If the difference is not significant, revert it.
  4. 4Watch the goroutine count during the load test and confirm it returns to baseline afterwards.
Check yourselfTake the Production test