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



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.Println to 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:

TEXT 📖 Display only
❌ 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

GO
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:

BASH
# 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)

GO
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))
}
▶ Try it Yourself
BASH
# 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)

GO
// 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()
    }
}
▶ Try it Yourself
BASH
# 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

GO
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))
}
▶ Try it Yourself
BASH
# 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

TEXT 📖 Display only
(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
💡 Tip: Functions with a high 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

GO
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))
}
▶ Try it Yourself
BASH
# 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

BASH
# 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

BASH
# 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
GO
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))
}
BASH
$ 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

GO
// 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...)
    }
}
▶ Try it Yourself
BASH
$ 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

GO
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)
}
▶ Try it Yourself
BASH
# 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

GO 📖 Display only
// 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))
}
78 logic lines (exceeds 40-line limit, display only)
100%
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]
💡 Tip: A three-step approach to performance optimization: measure → identify → optimize. Don't guess where the bottlenecks are—run 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

Q How do I start pprof?
A There are two ways: (1) HTTP طريقة: 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).
Q What is the difference between a CPU profile and a heap profile?
A A CPU profile samples "which functions the CPU is currently executing" (timed sampling) and is used to identify CPU hotspots. A heap profile samples "which objects have been allocated in memory" and is used to identify memory leaks and GC pressure. These are two distinct types of profiles and can be collected simultaneously.
Q What should I look for in a goroutine profile?
A Check the number and حالة of goroutines. Use 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.
Q How do I interpret the output of the -benchmem option in the benchmark?
A There are three columns: 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.
Q What is the difference between trace and pprof?
A pprof takes a "snapshot" (sampling) to answer "where is the bottleneck?"; trace records a "video" (حدث تدفق) to answer "why is it slow?" Trace provides a complete timeline of goroutine scheduling—when a goroutine runs, when it blocks, and when the GC pauses. First use pprof to identify the problem area, then use trace to analyze the root cause.
Q How do I use the race detector?
A 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.
Q What are some common Go performance bottlenecks?
A (1) Using + 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


📝 Exercises

  1. Basic (Difficulty ⭐): Write a program with performance issues (involving a large number of سلسلة concatenations using +), and enable the pprof HTTP endpoint. Run go tool pprof -http=:8081 to view the CPU profile and identify the hotspots. Then optimize the code by using strings.Builder and compare the CPU profiles before and after the change.

  2. Advanced (Difficulty ⭐⭐): Analyze JSON serialization performance using benchmark and pprof. Requirements: (1) Create a struct containing 100 fields; (2) Compare the performance of json.Marshal and json.Encoder; (3) Use -benchmem to view memory allocation; (4) Use -cpuprofile to generate a profile and analyze hotspots with go tool pprof.

  3. 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 -race to detect concurrency issues; (4) After fixing all issues, use pprof to verify that there are no more leaks; (5) Write a complete diagnostic report.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏