Advanced Profiling (pprof/trace)
Performance: Deep Dives with pprof & trace
Optimization without measurement is just guesswork. Go has arguably the best built-in profiling tools of any compiled language.
1. pprof: The Sampling Profiler
pprof creates a statistical profile of your program.
Usage
Add one line to your main:
import _ "net/http/pprof"
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// ... app code ...
}Now, while your app runs under load, grab a profile:
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30This opens a web UI. Look at: * Flame Graph: The widest bars are using the most CPU. * Alloc Space: Who allocated the most memory (Heap). * Alloc Objects: Who created the most count of objects (high GC pressure).
Mutex Profiling
If your CPU usage is low but performance sucks, you have contention. Enable mutex profiling in code: runtime.SetMutexProfileFraction(1) Then look at /debug/pprof/mutex to see who is waiting for locks.
2. Execution Tracer (go tool trace)
While pprof aggregates data, Trace shows you a timeline of every event. * When did a Goroutine start? * When did it block on a channel? * When did GC pause execution?
Capture a trace:
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5
go tool trace trace.outUse Case: Debugging latency spikes. You might see a 100ms gap where nothing ran. Zooming in, you might find a Stop-The-World GC event or a single mutex holding up 50 goroutines.
3. Benchmarks as Profiling Inputs
You can profile specific functions using benchmarks:
go test -bench=. -cpuprofile=cpu.out
go tool pprof -http=:8080 cpu.outSummary
- pprof: For “Where is my CPU/RAM going?”
- trace: For “Why is there latency?” and “What is the scheduler doing?”
- Optimization loop: Measure -> Fix -> Verify (Benchmark).
Worked example
Two algorithms, one clear allocation winner—profile-friendly benchmarks.
Save as join_test.go. Then:
go mod init example
go test -bench=. -benchmem
go test -bench=Builder -cpuprofile=cpu.out -benchmempackage main
import (
"strings"
"testing"
)
func joinPlus(parts []string) string {
s := ""
for _, p := range parts {
s += p
}
return s
}
func joinBuilder(parts []string) string {
var b strings.Builder
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}
var parts = []string{"go", "is", "fast", "enough", "when", "measured"}
func BenchmarkPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = joinPlus(parts)
}
}
func BenchmarkBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = joinBuilder(parts)
}
}Expected output: (Builder lower B/op and allocs/op)
BenchmarkPlus-8 ... ... ns/op ... B/op ... allocs/op
BenchmarkBuilder-8 ... ... ns/op ... B/op ~1 allocs/op
More examples
Emit a short execution trace from a benchmark.
go test -bench=Builder -trace=trace.out
# go tool trace trace.out// same package as above; no extra code required for -traceRunnable example
Save as alloc_test.go. Then:
go mod init example
go test -bench=. -benchmem
# optional: write a CPU profile for go tool pprof
go test -bench=SumPrealloc -cpuprofile=cpu.out -benchmempackage main
import "testing"
func sumAlloc(n int) int {
// allocates a new backing array every call
s := make([]int, 0)
for i := 0; i < n; i++ {
s = append(s, i)
}
total := 0
for _, v := range s {
total += v
}
return total
}
func sumPrealloc(n int) int {
s := make([]int, 0, n)
for i := 0; i < n; i++ {
s = append(s, i)
}
total := 0
for _, v := range s {
total += v
}
return total
}
func BenchmarkSumAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = sumAlloc(1024)
}
}
func BenchmarkSumPrealloc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = sumPrealloc(1024)
}
}Expected output: (SumPrealloc should show ~1 alloc/op vs many for SumAlloc)
BenchmarkSumAlloc-8 100000 12000 ns/op 20000 B/op 10 allocs/op
BenchmarkSumPrealloc-8 200000 6000 ns/op 8192 B/op 1 allocs/op
PASS
What to notice: Benchmarks are excellent profiling inputs—no long-running server required. Allocation differences show up clearly with -benchmem before you open a flame graph. For live services, the chapter’s net/http/pprof endpoints are the online equivalent.
Try next: go tool pprof -http=:8080 cpu.out after the profile command. Capture a short trace with go test -trace=trace.out -bench=SumPrealloc then go tool trace trace.out.