Performance Tuning
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=. -benchmemCPU Profiling
go test -cpuprofile=cpu.out -bench=.
go tool pprof cpu.outpprof 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.outHTTP Profiling
import _ "net/http/pprof"
// Endpoints:
// /debug/pprof/
// /debug/pprof/heap
// /debug/pprof/goroutine
// /debug/pprof/profileTracing
go test -trace=trace.out
go tool trace trace.outOptimization 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.BuilderAvoid 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=. -benchmempackage 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)' -benchmemRunnable example
Save as concat_test.go (benchmarks live in _test.go files). Then:
go mod init example
go test -bench=. -benchmempackage 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.