Go: Go Performance Analysis
Last updated: 2026-08-26
Performance analysis isn't a mystery—by using pprof, benchmark, and trace together, you can pinpoint CPU, memory, and concurrency bottlenecks in your Go programs.
When a Go API استجابة time goes from 50 ms to 5 s, do you add logs to your code to troubleshoot the issue, or do you use tools to pinpoint the problem? In this lesson, you'll master the full suite of tools for Go performance analysis.
1. You will learn
pprofstartup methods (HTTP / test / file)- CPU profile analysis of hotspot functions
- Heap profile analysis of memory allocation
- Analyzing Concurrency Issues with Goroutine Profiling
benchmarkbenchmark test +-benchmemtrace: Trace goroutine scheduling-raceData race detection
2. A True Story of a Backend Engineer
(1) Pain Point: API استجابة time went from 50 ms to 5 s; it took a week of logging to figure it out
Bob is a واجهة خلفية engineer on the payments team, and his API has been getting slower and slower:
"The payment interface was working fine a month ago, but this week it's taking 5 seconds to respond. I added 50 lines of log output, using
fmt.Printlnto timestamp the start and end of each دالة—I modified the code 10 times and deployed it 10 times, but I still couldn't find the problem. My boss asked me, 'It's been a week—what exactly is the problem?'"
His suspicions point toward:
❌ Database too slow? — But queries only take 2ms
❌ Downstream service timeout? — Called it and the response is normal
❌ Network latency? — All in the same datacenter
✅ Actual cause: string concatenation causing massive memory allocation + frequent GC
(2) Go Solution: pprof for Precise Debugging
import (
"net/http"
_ "net/http/pprof" // One line to enable pprof
)
func main() {
// pprof endpoints auto-registered at /debug/pprof/
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Business code continues running...
}
Then Bob ran:
# Collect 30-second CPU profile
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
# Result: flame graph shows strings.Builder only uses 2% CPU,
# while strings.Join + garbage collection uses 78% CPU!
(3) Benefits: Guessing vs. Tools
| Method | Time | Accuracy |
|---|---|---|
fmt.Println logging |
1 week (multiple deployments) | ❌ Guessing |
| pprof CPU profile | 30 seconds | ✅ Precise identification of hot functions |
| pprof Heap profile | 1 second | ✅ Memory allocation details down to line numbers |
3. How to Start pprof
▶ Example: HTTP Method (Most Common)
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof" // Import to auto-register pprof endpoints
"time"
)
func slowFunction() {
// Simulate a slow function
var result string
for i := 0; i < 100000; i++ {
result += fmt.Sprintf("%d ", i) // Bad string concatenation
}
}
func main() {
// Start pprof HTTP service (separate port, not exposed externally)
go func() {
log.Println("pprof listening on :6060")
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Business service
http.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) {
slowFunction()
fmt.Fprintln(w, "done")
})
log.Println("Business service listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
# pprof endpoints:
# http://localhost:6060/debug/pprof/ — index
# http://localhost:6060/debug/pprof/profile — CPU profile (default 30s)
# http://localhost:6060/debug/pprof/heap — Heap profile
# http://localhost:6060/debug/pprof/goroutine — goroutine info
# http://localhost:6060/debug/pprof/block — block analysis
# http://localhost:6060/debug/pprof/mutex — lock contention analysis
▶ Example: Testing Methods (benchmark + pprof)
// string_bench_test.go
package main
import (
"strings"
"testing"
)
// Bad approach: + concatenation
func BenchmarkStringPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 1000; j++ {
s += "a"
}
}
}
// Good approach: strings.Builder
func BenchmarkStringBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 1000; j++ {
sb.WriteString("a")
}
_ = sb.String()
}
}
// Good approach: pre-allocate
func BenchmarkStringBuilderPrealloc(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.Grow(1000)
for j := 0; j < 1000; j++ {
sb.WriteString("a")
}
_ = sb.String()
}
}
# Run benchmark (view memory allocations)
$ go test -bench=. -benchmem -count=3
# Generate CPU profile
$ go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof
# Analyze profile
$ go tool pprof -http=:8081 cpu.prof
4. CPU Profile
▶ Example: Identifying CPU hotspots
package main
import (
"log"
"net/http"
_ "net/http/pprof"
"strings"
)
func heavyCPU() string {
var sb strings.Builder
for i := 0; i < 10000; i++ {
sb.WriteString("hello")
sb.WriteString(" ")
sb.WriteString("world")
sb.WriteString("\n")
}
return sb.String()
}
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
http.HandleFunc("/cpu", func(w http.ResponseWriter, r *http.Request) {
result := heavyCPU()
w.Write([]byte(result[:100]))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
# Collect CPU profile (access /cpu endpoint multiple times within 30 seconds)
$ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
# CLI mode
$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
(pprof) top # Show Top 10 hot functions
(pprof) list main # View per-line time in the main package
(pprof) web # Open visualization in browser
(2) Interpreting pprof top Output
(pprof) top
Showing nodes accounting for 4.56s, 82.31% of 5.54s total
Dropped 28 nodes (cum <= 0.03s)
flat flat% sum% cum cum%
2.34s 42.24% 42.24% 2.34s 42.24% runtime.memmove
1.12s 20.22% 62.46% 1.12s 20.22% runtime.mallocgc
0.56s 10.11% 72.57% 0.56s 10.11% strings.(*Builder).copy
...
| Column | Meaning |
|---|---|
flat |
Time taken by the current function itself |
flat% |
Percentage of total time spent on "flat" |
sum% |
Cumulative percentage |
cum |
Time consumed by the current function and all subfunctions it calls |
cum% |
Percentage of total time accounted for by cum |
flat value are "slow on their own" (hotspots), while functions with a high cum but low flat value are "slow due to calls" (management issues). Optimize the functions with the highest flat values first—this will yield the quickest results.
5. Heap Profile
▶ Example: Identifying memory leaks
package main
import (
"log"
"net/http"
_ "net/http/pprof"
)
var leak []string // Global variable, never garbage collected
func memoryLeak() {
// Appends 10000 entries on each call, never clears
for i := 0; i < 10000; i++ {
leak = append(leak, "leaked string data")
}
}
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
http.HandleFunc("/leak", func(w http.ResponseWriter, r *http.Request) {
memoryLeak()
w.Write([]byte("leaked"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
# Collect heap profile (view current memory allocation)
$ go tool pprof -http=:8081 http://localhost:6060/debug/pprof/heap
# View functions with the most allocations
$ go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
(pprof) top
(pprof) list main.memoryLeak
(2) Heap View Mode
# Four viewing modes:
-inuse_space # Currently in-use memory (default)
-inuse_objects # Currently in-use كائن count
-alloc_space # Total allocated memory
-alloc_objects # Total allocated كائن count
# Use alloc_space to find leaks (see who allocates the most)
$ go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
| Mode | Purpose |
|---|---|
inuse_space |
Current memory usage (final result of leak detection) |
inuse_objects |
Number of current objects (for finding a large number of small objects) |
alloc_space |
Total allocation (to identify the root cause of frequent GCs) |
alloc_objects |
Total number of allocations (to identify short-lived objects) |
6. Goroutine Profile
▶ Example: Goroutine Leak Detection
# View goroutine count and status
$ go tool pprof http://localhost:6060/debug/pprof/goroutine
# View goroutine stack trace (text)
$ curl http://localhost:6060/debug/pprof/goroutine?debug=2
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"time"
)
func leakyGoroutine() {
ch := make(chan int)
go func() {
// This goroutine will never exit
val := <-ch // Blocks forever
fmt.Println(val)
}()
// ch will never receive data
}
func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
http.HandleFunc("/leak", func(w http.ResponseWriter, r *http.Request) {
leakyGoroutine()
w.Write([]byte("leaked"))
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
$ curl http://localhost:6060/debug/pprof/goroutine?debug=2
# The output shows each goroutine's stack trace:
# goroutine 5 [chan receive]:
# main.leakyGoroutine.func1()
# /app/main.go:14
# If you see many [chan receive] with no corresponding sender → leak
7. Benchmark and Trace
▶ Example: Benchmark + -benchmem
// bench_test.go
package main
import (
"encoding/json"
"testing"
)
type Data struct {
ID int `json:"id"`
Name سلسلة `json:"name"`
Email سلسلة `json:"email"`
}
// Benchmark: JSON serialization performance
func BenchmarkJSONMarshal(b *testing.B) {
data := Data{ID: 1, Name: "Alice", Email: "alice@example.com"}
for i := 0; i < b.N; i++ {
_, err := json.Marshal(data)
if err != nil {
b.Fatal(err)
}
}
}
// Benchmark: JSON serialization + pre-allocated مخزن مؤقت
func BenchmarkJSONMarshalBuffer(b *testing.B) {
data := Data{ID: 1, Name: "Alice", Email: "alice@example.com"}
buf := make([]byte, 0, 256)
for i := 0; i < b.N; i++ {
buf = buf[:0]
result, err := json.Marshal(data)
if err != nil {
b.Fatal(err)
}
buf = append(buf, result...)
}
}
$ go test -bench=. -benchmem -count=5 ./...
BenchmarkJSONMarshal-8 10000000 156.2 ns/op 48 B/op 1 allocs/op
BenchmarkJSONMarshalBuffer-8 10000000 158.1 ns/op 48 B/op 1 allocs/op
▶ Example: Trace
package main
import (
"fmt"
"os"
"runtime/trace"
"sync"
)
func main() {
// Create trace file
f, _ := os.Create("trace.out")
defer f.Close()
// Start trace
trace.Start(f)
defer trace.Stop()
// Run code under test
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
result := fibonacci(30)
fmt.Printf("Worker %d: %d\n", id, result)
}(i)
}
wg.Wait()
}
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
# After generating the trace file, view it in a browser
$ go tool trace trace.out
# Opens the browser, showing:
# - Goroutine analysis: how long each goroutine ran
# - Scheduling latency: when goroutines were scheduled
# - Network blocking: what goroutines are waiting for
# - System calls: when GC ran
8. Complete Example: Identifying "500 ms Slow Response"
▶ Example: Full Debugging Demo
// debug_demo.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"strings"
)
// ---------- Slow API ----------
type UserResponse struct {
ID int `json:"id"`
Name سلسلة `json:"name"`
Email سلسلة `json:"email"`
Bio سلسلة `json:"bio"`
}
// Bad version: سلسلة concatenation + heavy allocation
func generateUserJSON(userID int) []byte {
var bio strings.Builder
// Simulate generating a large amount of text
for i := 0; i < 1000; i++ {
bio.WriteString(fmt.Sprintf("Line %d: User data for ID %d with some additional info\n", i, userID))
}
resp := UserResponse{
ID: userID,
Name: fmt.Sprintf("User_%d", userID),
Email: fmt.Sprintf("user%d@example.com", userID),
Bio: bio.String(),
}
data, _ := json.Marshal(resp)
return data
}
// Optimized version: pre-allocation + reduced formatting
func generateUserJSONOptimized(userID int) []byte {
// Pre-allocate مخزن مؤقت
var bio strings.Builder
bio.Grow(50000) // Estimated size
for i := 0; i < 1000; i++ {
bio.WriteString("Line ")
bio.WriteString(fmt.Sprintf("%d", i)) // Can be further optimized with strconv.Itoa
bio.WriteString(": User data for ID ")
bio.WriteString(fmt.Sprintf("%d", userID))
bio.WriteString(" with some additional info\n")
}
resp := UserResponse{
ID: userID,
Name: "User_" + fmt.Sprintf("%d", userID),
Email: fmt.Sprintf("user%d@example.com", userID),
Bio: bio.String(),
}
data, _ := json.Marshal(resp)
return data
}
// ---------- Analysis workflow ----------
/*
Debugging workflow:
(1) Step 1: Start pprof HTTP خادم
go run main.go (automatically starts pprof on :6060)
(2) Step 2: Load test
# In another terminal, send continuous requests
while true; do curl http://localhost:8080/user/1 > /dev/فارغ; done
(3) Step 3: Collect CPU profile
go tool pprof -http=:8081 http://localhost:6060/debug/pprof/profile?seconds=30
(4) Step 4: View flame graph in browser
- Look for the widest color blocks → hot functions
- If you see runtime.memmove / runtime.mallocgc → excessive memory allocation
- Click main.generateUserJSON → view per-line code timing
(5) Step 5: View Heap profile
go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap
(pprof) top
*/
func main() {
// pprof
go func() {
log.Println("pprof on :6060")
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Business endpoint
http.HandleFunc("/user/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
var userID int
fmt.Sscanf(id, "%d", &userID)
data := generateUserJSON(userID)
w.Header().Set("Content-Type", "application/json")
w.Write(data)
})
log.Println("Service listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
flowchart TD
A[API slow response] --> B{Problem type?}
B -->|High CPU| C[pprof CPU profile]
B -->|High memory| D[pprof Heap profile]
B -->|Many goroutines| E[pprof goroutine profile]
B -->|Scheduling latency| F[go tool trace]
C --> C1[View flame graph]
C1 --> C2{Hot function?}
C2 -->|runtime.memmove| G[Reduce memory allocation]
C2 -->|Business function| H[Optimize algorithm / add cache]
D --> D1[View alloc_space]
D1 --> D2{Who allocates the most?}
D2 -->|strings.Builder| I[Pre-allocate with Grow]
D2 -->|Temporary objects| J[Use sync.Pool]
E --> E1[View goroutine stack]
E1 --> E2{Goroutine status?}
E2 -->|chan receive blocked| K[Check channel sender]
E2 -->|IO wait| L[Check connection pool]
F --> F1[View goroutine analysis]
F1 --> F2{Scheduling latency?}
F2 -->|GC pause| M[Reduce memory allocation]
F2 -->|System calls| N[Optimize IO operations]
pprof to gather data first, then use that data to make decisions. The most common Go performance bottlenecks are excessive memory allocation (high GC pressure) and inefficient سلسلة concatenation.
❓ FAQ
import _ "net/http/pprof", then start the HTTP service; the endpoint is automatically registered at /debug/pprof/; (2) Test mode: go test -cpuprofile=cpu.prof -memprofile=mem.prof. In a production environment, use the HTTP طريقة via a dedicated port (not exposed to the public).curl /debug/pprof/goroutine?debug=2 to view the stack trace for each goroutine. A large number of goroutines in the [chan receive] state may indicate a channel leak. A large number of goroutines in the [IO wait] state may indicate an insufficient connection pool.-benchmem option in the benchmark?ns/op (time per operation), B/op (bytes allocated per operation), and allocs/op (number of allocations per operation). Optimization goal: Reduce allocs/op (number of allocations), because GC time is related to the number of objects.go run -race main.go or go test -race ./.... The race detector detects data races at runtime—concurrent reads and writes to the same متغير (with at least one write) will trigger a warning. It is recommended to always enable -race in CI/CD, but it significantly slows down execution (5–20 times), so do not enable it in production environments.+ for سلسلة concatenation instead of strings.Builder; (2) Forgetting to preallocate slice/map sizes; (3) Frequent JSON serialization/deserialization; (4) Goroutine leaks causing resources to remain unreleased; (5) Blocking due to improper channel usage; (6) Intense lock contention. Use pprof to identify these issues and optimize them one by one.📖 Summary
- pprof startup:
import _ "net/http/pprof"+ HTTP endpoint - CPU profile:
/debug/pprof/profile?seconds=30 - Heap profile:
/debug/pprof/heap(four modes) - Goroutine profile:
/debug/pprof/goroutine?debug=2 - benchmark:
go test -bench=. -benchmem -cpuprofile=... - trace:
go tool trace trace.out(to view scheduling latency) - race detector:
go test -race ./... - Optimized process: Measure → Identify → Optimize—no guesswork required
📝 Exercises
-
Basic (Difficulty ⭐): Write a program with performance issues (involving a large number of سلسلة concatenations using
+), and enable the pprof HTTP endpoint. Rungo tool pprof -http=:8081to view the CPU profile and identify the hotspots. Then optimize the code by usingstrings.Builderand compare the CPU profiles before and after the change. -
Advanced (Difficulty ⭐⭐): Analyze JSON serialization performance using benchmark and pprof. Requirements: (1) Create a struct containing 100 fields; (2) Compare the performance of
json.Marshalandjson.Encoder; (3) Use-benchmemto view memory allocation; (4) Use-cpuprofileto generate a profile and analyze hotspots withgo tool pprof. -
Challenge (Difficulty ⭐⭐⭐): Diagnose and fix a program with memory leaks. You are provided with a piece of Go code that contains memory leaks (goroutine leaks + slice leaks). Requirements: (1) Use pprof heap profiling to locate the source of the leaks; (2) Use a goroutine profile to confirm the number of leaks; (3) Use
-raceto detect concurrency issues; (4) After fixing all issues, use pprof to verify that there are no more leaks; (5) Write a complete diagnostic report.