Go is already fast compared to many other languages, but performance still matters when you’re running services at scale. Optimizing Go code isn’t about guessing where the bottlenecks are — it’s about measuring, profiling, and improving based on real data.
This guide covers practical techniques for benchmarking, profiling, and tuning Go applications.
1. Benchmarking with testing.B
Go has built-in support for writing benchmarks using the testing
package.
Example:
package main
import (
"strings"
"testing"
)
func concatWithPlus(a, b string) string {
return a + b
}
func concatWithBuilder(a, b string) string {
var sb strings.Builder
sb.WriteString(a)
sb.WriteString(b)
return sb.String()
}
func BenchmarkConcatPlus(b *testing.B) {
for i := 0; i < b.N; i++ {
concatWithPlus("hello", "world")
}
}
func BenchmarkConcatBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
concatWithBuilder("hello", "world")
}
}
Run benchmarks with:
go test -bench=. -benchmem
Output includes:
- Execution time per operation.
- Memory allocations per operation.
2. Profiling with pprof
The pprof
package helps you find performance bottlenecks in CPU usage, memory, and goroutines.
Add profiling support to your program:
import (
_ "net/http/pprof"
"net/http"
)
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// Your app code here...
}
Run your program, then open:
http://localhost:6060/debug/pprof/
Or capture CPU profile:
go tool pprof http://localhost:6060/debug/pprof/profile
From here, you can generate flame graphs to visualize performance hotspots.
3. Memory Profiling
Memory leaks or excessive allocations can slow down apps.
Use:
go tool pprof http://localhost:6060/debug/pprof/heap
Look for functions with high allocations and consider:
- Using
sync.Pool
for object reuse. - Pre-allocating slices with
make([]T, 0, n)
. - Reducing unnecessary
string
→[]byte
conversions.
4. Common Performance Patterns
Avoiding Unnecessary Allocations
// Bad
s := fmt.Sprintf("%s%s", a, b)
// Better
s := a + b
Use Buffered Channels and I/O
writer := bufio.NewWriter(file)
defer writer.Flush()
Minimize Goroutine Leaks
Always cancel goroutines with context.WithCancel
or defer close(ch)
.
5. Practical Workflow for Performance Tuning
- Benchmark – Write micro-benchmarks for critical code paths.
- Profile – Run
pprof
to find real bottlenecks. - Optimize – Apply focused improvements (e.g., reduce allocations, batch operations).
- Re-benchmark – Measure again to confirm improvements.
- Repeat – Optimization is iterative.
6. When to Optimize
Not every piece of code needs micro-optimization. Focus on:
- Hot paths – functions called thousands of times per second.
- Memory-heavy components – caches, parsers, JSON handling.
- I/O bottlenecks – database queries, network requests.
Premature optimization wastes time — measure first, optimize only where it matters.
Conclusion
Optimizing Go applications isn’t about rewriting everything from scratch. By using benchmarks to measure, pprof to identify bottlenecks, and targeted tuning, you can achieve dramatic performance improvements with minimal code changes.
The key lesson: don’t guess, measure. Let the data guide your optimization decisions, and your Go applications will scale smoothly without unnecessary complexity.