Go Performance Optimization: Profiling, Benchmarks & Memory 2026
Go performance optimization guide: pprof CPU/memory profiling, writing benchmarks, escape analysis, reducing allocations, sync.Pool, goroutine leaks detection, and performance testing.
Go is fast out of the box, but production services often have performance headroom you havenβt exploited. This guide walks through systematic Go performance optimization: benchmark first, profile second, fix third. No premature optimization β every change is measured.
The Optimization Process
- Benchmark β establish a baseline, write reproducible measurements
- Profile β find where time and memory are actually spent
- Analyze β understand root cause (allocation, syscall, contention)
- Fix β targeted change based on evidence
- Verify β measure again, confirm improvement
Writing Benchmarks
Goβs built-in benchmark framework is powerful. Benchmarks live in _test.go files and use testing.B.
package mypackage
import (
"testing"
"strings"
"bytes"
)
// Basic benchmark
func BenchmarkStringConcatenation(b *testing.B) {
for b.Loop() { // Go 1.24+: b.Loop() preferred over i < b.N
var s string
for j := 0; j < 100; j++ {
s += "hello"
}
_ = s
}
}
// Better approach
func BenchmarkStringBuilder(b *testing.B) {
for b.Loop() {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("hello")
}
_ = sb.String()
}
}
// With memory stats
func BenchmarkJSONMarshal(b *testing.B) {
b.ReportAllocs() // show allocations per op
input := generateTestData()
for b.Loop() {
data, err := json.Marshal(input)
if err != nil {
b.Fatal(err)
}
_ = data
}
}
// Table-driven benchmarks
func BenchmarkHashFunctions(b *testing.B) {
data := []byte("benchmark input data " + strings.Repeat("x", 1000))
benchmarks := []struct {
name string
fn func([]byte) uint64
}{
{"fnv32", fnv32Hash},
{"xxhash", xxhash.Sum64},
{"murmur3", murmur3.Sum64},
}
for _, bm := range benchmarks {
b.Run(bm.name, func(b *testing.B) {
b.SetBytes(int64(len(data)))
for b.Loop() {
_ = bm.fn(data)
}
})
}
}
Running Benchmarks
go test -bench=. ./... # run all benchmarks
go test -bench=BenchmarkString -benchmem ./... # specific + memory stats
go test -bench=. -benchtime=5s ./... # longer run for stable results
go test -bench=. -count=5 ./... # run 5 times for variance
# Compare before/after with benchstat
go install golang.org/x/perf/cmd/benchstat@latest
go test -bench=. -count=5 > before.txt
# make your change
go test -bench=. -count=5 > after.txt
benchstat before.txt after.txt
benchstat output:
β before.txt β after.txt β
β sec/op β sec/op vs base β
String/v1 β 1.234Β΅ Β± 1%β 0.891Β΅ Β± 2% -27.8% (p=0.001)
String/v2 β 0.912Β΅ Β± 3%β 0.889Β΅ Β± 1% -2.5% (p=0.042)
pprof CPU Profiling
HTTP Endpoint Profiling
import (
_ "net/http/pprof"
"net/http"
"log"
)
func main() {
// Enable pprof endpoint on a separate port
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... rest of your server
}
# Collect 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Interactive pprof shell
(pprof) top10 # top 10 functions by cumulative time
(pprof) top10 -cum # sort by cumulative (includes callees)
(pprof) web # open SVG flame graph in browser
(pprof) list funcName # show source with annotations
(pprof) traces # call traces
Benchmark-Based Profiling
# Generate profile from benchmark
go test -bench=BenchmarkFoo -cpuprofile=cpu.prof ./...
go test -bench=BenchmarkFoo -memprofile=mem.prof ./...
# Analyze
go tool pprof cpu.prof
go tool pprof -http=:8080 cpu.prof # web UI (recommended)
Memory Profiling and Allocation Reduction
Heap Profile
# Capture heap
go tool pprof http://localhost:6060/debug/pprof/heap
(pprof) top10 -inuse_space # largest in-use allocations
(pprof) top10 -alloc_space # cumulative allocations since start
(pprof) web inuse_space
Escape Analysis
The compiler moves variables to the heap when it canβt prove they wonβt outlive their stack frame. Each heap allocation means GC pressure.
go build -gcflags='-m' ./... # show escape analysis
go build -gcflags='-m -m' ./... # verbose (shows reason)
Output:
./handler.go:45:13: &user escapes to heap
./handler.go:62:20: make([]byte, n) does not escape
./handler.go:78:15: string(b) escapes to heap
Reducing escapes:
// Bad: causes heap allocation
func processRequest(r *http.Request) *Result {
result := Result{} // might escape
result.process(r)
return &result // taking address forces heap allocation
}
// Better: return by value when small
func processRequest(r *http.Request) Result {
var result Result
result.process(r)
return result // stack allocated, copied on return
}
// For large structs: pass in a pointer to caller-allocated value
func processRequest(r *http.Request, result *Result) {
result.process(r)
}
sync.Pool: Reuse Expensive Objects
sync.Pool reduces GC pressure by reusing allocated objects. Use it for frequently allocated, short-lived objects.
import "sync"
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 4096)
},
}
func processData(data []byte) []byte {
// Get a buffer from pool
buf := bufferPool.Get().([]byte)
buf = buf[:0] // reset without deallocating
defer bufferPool.Put(buf)
// Use the buffer
buf = append(buf, data...)
buf = transform(buf)
result := make([]byte, len(buf))
copy(result, buf)
return result
}
// JSON encoder pool
var encoderPool = sync.Pool{
New: func() interface{} {
return json.NewEncoder(io.Discard)
},
}
// bytes.Buffer pool (very common pattern)
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func renderTemplate(tmpl *template.Template, data interface{}) ([]byte, error) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
if err := tmpl.Execute(buf, data); err != nil {
return nil, err
}
result := make([]byte, buf.Len())
copy(result, buf.Bytes())
return result, nil
}
Pool impact:
| Scenario | Without Pool | With Pool | Improvement |
|---|---|---|---|
| HTTP handler (1K req/s) | 45 allocs/op | 8 allocs/op | 82% |
| JSON encoding | 3.2ms | 0.8ms | 75% |
| GC pause frequency | Every 500ms | Every 2.3s | 4.6x |
Value vs Pointer Receivers
The choice between value and pointer receivers affects allocations and performance.
type Point struct{ X, Y float64 }
// Value receiver: copy of Point on each call
// Good for small structs, immutable operations
func (p Point) Distance(other Point) float64 {
dx, dy := p.X-other.X, p.Y-other.Y
return math.Sqrt(dx*dx + dy*dy)
}
// Pointer receiver: no copy, modifies original
// Required for mutation, preferred for large structs
func (p *Point) Scale(factor float64) {
p.X *= factor
p.Y *= factor
}
// Rule of thumb:
// - Small structs (<= 2-3 fields): value receiver OK
// - Large structs (> 3 fields): pointer receiver
// - Contains mutex/sync primitives: always pointer receiver
// - Modifies state: always pointer receiver
strings.Builder for String Assembly
// Bad: O(nΒ²) allocations
func joinStrings(parts []string) string {
result := ""
for _, p := range parts {
result += p // new allocation on each iteration
}
return result
}
// Good: single allocation
func joinStrings(parts []string) string {
var sb strings.Builder
// Preallocate if you know total size
total := 0
for _, p := range parts {
total += len(p)
}
sb.Grow(total)
for _, p := range parts {
sb.WriteString(p)
}
return sb.String()
}
// Use strings.Join when separator is needed
result := strings.Join(parts, ", ")
Goroutine Leak Detection
Goroutine leaks are a common production issue. Use goleak to catch them in tests.
go get go.uber.org/goleak
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m) // fails if goroutines leak
}
func TestMyFunc(t *testing.T) {
defer goleak.VerifyNone(t) // per-test check
// This leaks a goroutine if done channel is never closed
done := make(chan struct{})
go func() {
for {
select {
case <-done:
return
case <-time.After(1 * time.Second):
process()
}
}
}()
// Bug: forgot to close(done) or send to done
}
Common Goroutine Leak Patterns
// Leak: channel send blocks forever
func processItems(items []string) <-chan Result {
results := make(chan Result) // unbuffered!
go func() {
for _, item := range items {
results <- process(item) // blocks if nobody reads
}
close(results)
}()
return results
}
// Fix: buffered channel or ensure consumer always reads
results := make(chan Result, len(items))
// Leak: goroutine waits on context that's never cancelled
func startWorker(ctx context.Context) {
go func() {
select {
case <-ctx.Done():
return
case <-dataChannel:
process()
}
}()
}
// Make sure the context is always eventually cancelled
Context Cancellation for Timeouts
func fetchWithTimeout(url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // ALWAYS defer cancel to prevent context leak
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Performance Testing with benchstat
# Run benchmarks multiple times (reduces noise)
go test -bench=BenchmarkHandler -benchmem -count=10 > before.txt
# After optimization
go test -bench=BenchmarkHandler -benchmem -count=10 > after.txt
# Statistical comparison
benchstat before.txt after.txt
# Output:
# goos: linux
# goarch: amd64
# β before.txt β after.txt β
# β sec/op β sec/op vs base β
# Handler β 125.4Β΅ Β± 2% β 48.2Β΅ Β± 1% -61.6% (p=0.000 n=10)
#
# β before.txt β after.txt β
# β B/op β B/op vs base β
# Handler β 2.40Ki Β± 0% β 0.48Ki Β± 0% -80.0% (p=0.000 n=10)
#
# β before.txt β after.txt β
# β allocs/op β allocs/op vs base β
# Handler β 32.00 Β± 0% β 6.00 Β± 0% -81.3% (p=0.000 n=10)
The golden rule of Go performance: measure first. The GC and escape analyzer are sophisticated β your intuitions about whatβs slow are often wrong. Let pprof tell you where to look.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.