Performance Tuning

Updated

July 30, 2026

Overview

Go provides built-in profiling tools for CPU, memory, and execution analysis.

Benchmarks

func BenchmarkFoo(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Foo()
    }
}
go test -bench=. -benchmem

CPU Profiling

go test -cpuprofile=cpu.out -bench=.
go tool pprof cpu.out

pprof Commands

(pprof) top10          # Top functions
(pprof) list FuncName  # Annotated source
(pprof) web            # Visual graph

Memory Profiling

go test -memprofile=mem.out -bench=.
go tool pprof -alloc_space mem.out

HTTP Profiling

import _ "net/http/pprof"

// Endpoints:
// /debug/pprof/
// /debug/pprof/heap
// /debug/pprof/goroutine
// /debug/pprof/profile

Tracing

go test -trace=trace.out
go tool trace trace.out

Optimization Tips

Reduce Allocations

// Preallocate
s := make([]int, 0, expectedSize)

// Reuse with sync.Pool
var pool = sync.Pool{New: func() any { return &Buffer{} }}

// strings.Builder for concatenation
var b strings.Builder

Avoid Copying

// Pass pointer for large structs
func process(data *LargeStruct)

// Use slices instead of arrays
func process(data []byte)

Summary

Tool Purpose
go test -bench Benchmarking
go tool pprof Profile analysis
go tool trace Execution tracing
-gcflags=-m Escape analysis

Worked example

Slice preallocation vs grow-from-zero (benchmark pair).

Save as slice_test.go. Then:

go mod init example
go test -bench=. -benchmem
package main

import "testing"

func grow(n int) []int {
    var s []int
    for i := 0; i < n; i++ {
        s = append(s, i)
    }
    return s
}

func prealloc(n int) []int {
    s := make([]int, 0, n)
    for i := 0; i < n; i++ {
        s = append(s, i)
    }
    return s
}

func BenchmarkGrow(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = grow(1024)
    }
}

func BenchmarkPrealloc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = prealloc(1024)
    }
}

Expected output: (Prealloc far fewer allocs)

BenchmarkGrow-8           ...   ... ns/op     ... B/op   ~10 allocs/op
BenchmarkPrealloc-8       ...   ... ns/op    8192 B/op     1 allocs/op

More examples

sync.Pool buffer reuse microbench.

package main

import (
    "sync"
    "testing"
)

var pool = sync.Pool{New: func() any { return make([]byte, 0, 256) }}

func BenchmarkPool(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buf := pool.Get().([]byte)
        buf = buf[:0]
        buf = append(buf, "hello"...)
        pool.Put(buf)
    }
}

func BenchmarkAlloc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        buf := make([]byte, 0, 256)
        buf = append(buf, "hello"...)
        _ = buf
    }
}
go test -bench='Benchmark(Pool|Alloc)' -benchmem

Runnable example

Save as concat_test.go (benchmarks live in _test.go files). Then:

go mod init example
go test -bench=. -benchmem
package main

import (
    "strings"
    "testing"
)

func concatPlus(n int) string {
    s := ""
    for i := 0; i < n; i++ {
        s += "x"
    }
    return s
}

func concatBuilder(n int) string {
    var b strings.Builder
    b.Grow(n)
    for i := 0; i < n; i++ {
        b.WriteByte('x')
    }
    return b.String()
}

func BenchmarkConcatPlus(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = concatPlus(64)
    }
}

func BenchmarkConcatBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = concatBuilder(64)
    }
}

Expected output: (numbers vary by machine; Builder should allocate far less)

goos: ...
goarch: ...
pkg: example
BenchmarkConcatPlus-8             200000          6000 ns/op        2000 B/op         60 allocs/op
BenchmarkConcatBuilder-8        10000000           100 ns/op          64 B/op          1 allocs/op
PASS

What to notice: -benchmem reports B/op and allocs/op—often more actionable than raw ns. Pre-sizing (Grow) and builders beat naive += on hot paths. Always re-benchmark after a “fix.”

Try next: Add BenchmarkConcatPrealloc using make([]byte, 0, n). Profile with go test -bench=Builder -cpuprofile=cpu.out and open go tool pprof cpu.out.