Understanding and optimizing performance is crucial for building efficient Go applications. Go provides excellent built-in profiling tools to help identify bottlenecks and optimize your code.
Go includes several profiling tools in the standard library and toolchain.
Profiling is important because it helps identify bottlenecks (find where your code spends time) and detect memory issues (memory leaks and excessive allocations).
Available profiling tools
Profiling is enabled via HTTP endpoints or programmatically:
package main
import (
"log"
"net/http"
_ "net/http/pprof" // Registers profiling handlers
)
func main() {
// Start HTTP server for profiling
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// Your application code
}
Then access profiles at:
http://localhost:6060/debug/pprof/ - Profile indexhttp://localhost:6060/debug/pprof/heap - Heap profilehttp://localhost:6060/debug/pprof/profile - CPU profileCPU profiling shows where your program spends CPU time.
CPU profiling matters because it helps find hot spots (functions that consume most CPU), identifies optimization targets (know what to optimize), and reveals inefficiencies (find expensive operations).
Collect CPU profiles using:
Method 1: HTTP endpoint
import _ "net/http/pprof"
// Access http://localhost:6060/debug/pprof/profile?seconds=30
// Downloads a 30-second CPU profile
Method 2: Programmatic
package main
import (
"os"
"runtime/pprof"
)
func main() {
f, _ := os.Create("cpu.prof")
defer f.Close()
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
// Your code to profile
doWork()
}
Analyze CPU profiles using:
go tool pprof cpu.prof
# Or
go tool pprof http://localhost:6060/debug/pprof/profile
Common pprof commands
(pprof) top # Show top functions by CPU time
(pprof) top10 # Show top 10 functions
(pprof) list FuncName # Show source code with CPU time
(pprof) web # Generate SVG visualization
(pprof) png # Generate PNG visualization
Example CPU profile analysis
(pprof) top
Showing nodes accounting for 2.50s, 100% of 2.50s total
flat flat% sum% cum cum%
1.50s 60.00% 60.00% 1.50s 60.00% main.expensiveFunction
1.00s 40.00% 100.00% 1.00s 40.00% main.anotherFunction
Understanding the output
Memory profiling shows memory allocation patterns and helps identify memory leaks.
Memory profiling matters because it helps find memory leaks (allocations that aren't freed), identifies excessive allocations (unnecessary allocations), and helps understand memory consumption patterns.
Collect memory profiles using:
package main
import (
"os"
"runtime"
"runtime/pprof"
)
func main() {
f, _ := os.Create("mem.prof")
defer f.Close()
// Your code
doWork()
// Write memory profile
runtime.GC() // Force GC before profiling
pprof.WriteHeapProfile(f)
}
Or via HTTP
import _ "net/http/pprof"
// Access http://localhost:6060/debug/pprof/heap
To analyze a memory profile, use:
go tool pprof mem.prof
Memory profile commands
(pprof) top # Top memory allocators
(pprof) top -cum # Top by cumulative allocations
(pprof) list FuncName # Show allocations in function
(pprof) alloc_space # Show allocations by space
(pprof) alloc_objects # Show allocations by count
Example memory profile
(pprof) top
Showing nodes accounting for 512.50MB, 100% of 512.50MB total
flat flat% sum% cum cum%
256.25MB 50.00% 50.00% 256.25MB 50.00% main.allocateMemory
256.25MB 50.00% 100.00% 256.25MB 50.00% main.anotherAllocation
Detecting memory leaks
Compare profiles at different times:
# Take profile at time T1
go tool pprof http://localhost:6060/debug/pprof/heap
# After some time, take another profile
go tool pprof http://localhost:6060/debug/pprof/heap
# Compare
go tool pprof -base=heap1.prof heap2.prof
Benchmarks measure code performance and help validate optimizations.
Benchmarking is important because it helps catch performance degradation (performance regression), verify that optimizations actually help (optimization validation), and compare different implementations (comparison).
Basic benchmark
func BenchmarkFunction(b *testing.B) {
for i := 0; i < b.N; i++ {
Function()
}
}
Running benchmarks
go test -bench=. # Run all benchmarks
go test -bench=BenchmarkFunction # Run specific benchmark
go test -bench=. -benchmem # Include memory stats
go test -bench=. -cpuprofile=cpu.prof # CPU profile during benchmark
Benchmark output
BenchmarkFunction-8 10000000 120 ns/op 16 B/op 1 allocs/op
Comparing benchmarks
func BenchmarkOldWay(b *testing.B) {
for i := 0; i < b.N; i++ {
oldImplementation()
}
}
func BenchmarkNewWay(b *testing.B) {
for i := 0; i < b.N; i++ {
newImplementation()
}
}
Benchmark with setup
func BenchmarkWithSetup(b *testing.B) {
// Setup (not timed)
data := prepareData()
b.ResetTimer() // Reset timer after setup
for i := 0; i < b.N; i++ {
process(data)
}
}
Setup code shouldn't be included in benchmark timing. ResetTimer() excludes setup from measurements, ensuring only the code being benchmarked is measured.
Benchmark different sizes
func BenchmarkAppend(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("size-%d", size), func(b *testing.B) {
slice := make([]int, 0, size)
for i := 0; i < b.N; i++ {
slice = append(slice, i)
if len(slice) >= size {
slice = slice[:0]
}
}
})
}
}
1. Reduce allocations
// ❌ BAD: Many allocations
func process(items []Item) []Result {
results := []Result{} // Allocates
for _, item := range items {
results = append(results, processItem(item)) // May reallocate
}
return results
}
// ✅ GOOD: Pre-allocate
func process(items []Item) []Result {
results := make([]Result, 0, len(items)) // Pre-allocate capacity
for _, item := range items {
results = append(results, processItem(item))
}
return results
}
2. Use sync.Pool for temporary objects
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func process() {
buf := bufferPool.Get().(*bytes.Buffer)
defer bufferPool.Put(buf)
buf.Reset()
// Use buf
}
3. Avoid unnecessary conversions
// ❌ BAD: Unnecessary conversion
func process(s string) {
b := []byte(s) // Allocation
// Use b
}
// ✅ GOOD: Work with string directly
func process(s string) {
// Use s directly, or use unsafe if really needed
}
4. Use strings.Builder for string concatenation
// ❌ BAD: Many allocations
func buildString(parts []string) string {
result := ""
for _, part := range parts {
result += part // Allocates new string each time
}
return result
}
// ✅ GOOD: Use Builder
func buildString(parts []string) string {
var builder strings.Builder
builder.Grow(len(parts) * 10) // Pre-allocate if you know size
for _, part := range parts {
builder.WriteString(part)
}
return builder.String()
}
5. Prefer value receivers for small structs
// ✅ GOOD: Value receiver for small struct
type Point struct {
X, Y int
}
func (p Point) Distance() float64 {
return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
// ⚠️ CONSIDER: Pointer receiver for large structs
type LargeStruct struct {
// Many fields
}
func (s *LargeStruct) Process() {
// ...
}
6. Use appropriate data structures
// ✅ GOOD: Use map for lookups
func find(items []Item, id int) *Item {
index := make(map[int]*Item, len(items))
for i := range items {
index[items[i].ID] = &items[i]
}
return index[id]
}
// ❌ BAD: Linear search
func find(items []Item, id int) *Item {
for i := range items {
if items[i].ID == id {
return &items[i]
}
}
return nil
}
7. Profile before optimizing
// Always measure first
func BenchmarkCurrent(b *testing.B) {
// Current implementation
}
func BenchmarkOptimized(b *testing.B) {
// Optimized implementation
}
// Compare results before deciding
8. Use compiler optimizations
go build -gcflags="-m" # See escape analysis
go build -ldflags="-s -w" # Strip debug info (smaller binary)
9. Consider escape analysis
// Understanding where values are allocated
func process() {
x := 42 // May escape to heap
// Use x
}
10. Use appropriate concurrency
// ✅ GOOD: Worker pool for controlled concurrency
func process(items []Item) {
const numWorkers = 10
jobs := make(chan Item, len(items))
var wg sync.WaitGroup
for w := 0; w < numWorkers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range jobs {
processItem(item)
}
}()
}
for _, item := range items {
jobs <- item
}
close(jobs)
wg.Wait()
}
Performance optimization workflow
Best practices
Profiling and optimization are iterative processes. Use the tools Go provides to identify bottlenecks, apply optimizations, and validate improvements. Remember: premature optimization is the root of all evil—measure first, then optimize.