Introduction#
Go ships with a built-in profiler (pprof) that measures CPU usage, memory allocation, goroutine counts, and mutex contention. Unlike external profilers, pprof integrates directly into your binary and can be enabled in production with minimal overhead.
Enabling pprof in HTTP Servers#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| package main
import (
"net/http"
_ "net/http/pprof" // registers /debug/pprof/* handlers
"log"
)
func main() {
// Application server
go func() {
http.ListenAndServe(":8080", appRouter())
}()
// Profiling server — bind to localhost only, never expose publicly
log.Fatal(http.ListenAndServe("localhost:6060", nil))
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Capture 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Capture heap profile
go tool pprof http://localhost:6060/debug/pprof/heap
# Goroutine dump (find goroutine leaks)
curl -s http://localhost:6060/debug/pprof/goroutine?debug=2
# Block profile (where goroutines block on channel/mutex)
go tool pprof http://localhost:6060/debug/pprof/block
# Mutex contention
go tool pprof http://localhost:6060/debug/pprof/mutex
|
Analyzing CPU Profiles#
1
2
3
4
5
6
7
8
9
10
11
12
| # Interactive pprof session
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Inside pprof:
(pprof) top10 # top 10 functions by CPU time
(pprof) top10 -cum # top 10 by cumulative time (includes callees)
(pprof) list ParseJSON # show annotated source for ParseJSON function
(pprof) web # open SVG flame graph in browser
(pprof) pdf # export PDF flame graph
# One-liner: capture and open flame graph
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=10
|
Benchmarks with pprof#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // bench_test.go
package mypackage
import (
"testing"
)
func BenchmarkParseRequest(b *testing.B) {
data := []byte(`{"user_id":42,"items":[{"id":1,"qty":2}]}`)
b.ResetTimer()
for i := 0; i < b.N; i++ {
ParseRequest(data)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| # Run benchmark and capture CPU + memory profiles
go test -bench=BenchmarkParseRequest -benchmem \
-cpuprofile=cpu.prof \
-memprofile=mem.prof \
-count=3
# Analyze CPU profile
go tool pprof cpu.prof
# Analyze memory allocations
go tool pprof mem.prof
(pprof) top10 -cum -sample_index=alloc_space
|
Memory Profiling#
1
2
3
4
5
6
7
| // Force a GC before capturing heap profile for cleaner data
import "runtime"
func captureHeapProfile() {
runtime.GC()
// Now take the profile
}
|
1
2
3
4
5
6
7
8
9
| # Heap profile: what is currently alive
go tool pprof http://localhost:6060/debug/pprof/heap
# Allocation profile: what has been allocated (including GC'd)
go tool pprof http://localhost:6060/debug/pprof/allocs
# Inside pprof — find allocation hotspots:
(pprof) top10 -sample_index=alloc_space
(pprof) list json.Marshal
|
Detecting Goroutine Leaks#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // A goroutine leak: goroutine started but never exits
func leakyHandler(w http.ResponseWriter, r *http.Request) {
ch := make(chan int)
go func() {
v := <-ch // blocks forever if nothing sends to ch
_ = v
}()
w.Write([]byte("ok"))
// ch is garbage collected but goroutine is still blocked
}
// Fix: use context for cancellation
func fixedHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ch := make(chan int, 1)
go func() {
select {
case v := <-ch:
_ = v
case <-ctx.Done():
return // exits when request completes
}
}()
w.Write([]byte("ok"))
}
|
1
2
3
4
5
6
| # Monitor goroutine count over time
watch -n5 'curl -s http://localhost:6060/debug/pprof/goroutine?debug=1 | head -5'
# If count grows steadily: goroutine leak
# Detailed goroutine dump to find what's blocking
curl http://localhost:6060/debug/pprof/goroutine?debug=2 | head -100
|
Continuous Profiling in Production#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Send profiles to a profiling backend (e.g., Pyroscope)
import "github.com/grafana/pyroscope-go"
pyroscope.Start(pyroscope.Config{
ApplicationName: "my-service",
ServerAddress: "http://pyroscope:4040",
ProfileTypes: []pyroscope.ProfileType{
pyroscope.ProfileCPU,
pyroscope.ProfileAllocObjects,
pyroscope.ProfileAllocSpace,
pyroscope.ProfileInuseObjects,
pyroscope.ProfileInuseSpace,
},
})
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // BAD: allocating in hot path
func processItems(items []string) []Result {
results := []Result{} // grows, causes reallocations
for _, item := range items {
results = append(results, process(item))
}
return results
}
// GOOD: pre-allocate
func processItems(items []string) []Result {
results := make([]Result, 0, len(items)) // pre-allocate exact size
for _, item := range items {
results = append(results, process(item))
}
return results
}
// BAD: string concatenation in loop
func buildQuery(parts []string) string {
result := ""
for _, p := range parts {
result += p + " " // allocates new string each iteration
}
return result
}
// GOOD: strings.Builder
func buildQuery(parts []string) string {
var b strings.Builder
b.Grow(len(parts) * 10) // pre-estimate capacity
for _, p := range parts {
b.WriteString(p)
b.WriteByte(' ')
}
return b.String()
}
|
Conclusion#
Enable pprof on a localhost-only port in production and capture profiles during load spikes. CPU profiles show where time is spent; heap profiles show allocation hotspots; goroutine dumps reveal leaks. The go tool pprof -http web UI with flame graphs is the most productive interface. For continuous visibility, integrate with Pyroscope or Parca for always-on profiling.