Go Benchmarking — Performance Testing with testing.B and Benchmark Functions
In this tutorial, you will learn about Go Benchmarking. We cover key concepts, practical examples, and best practices to help you master this topic.
Go benchmarking measures performance with testing.B for timing, b.N for iterations, and benchstat for statistical comparison of benchmark results.
What You'll Learn
- Writing benchmarks
- Benchmark comparison
- Memory allocation profiling
- Benchmark best practices
Why It Matters
Benchmarks prevent performance regressions. Docker benchmarks build performance. Kubernetes benchmarks scheduler latency. DodaZIP benchmarks compression throughput.
Real-World Use
Performance regression detection, optimization validation, library comparison, capacity planning.
flowchart LR
A["Benchmarking"] --> B["Benchmark Functions"]
A --> C["Memory Profiling"]
A --> D["benchstat"]
A --> E["Optimization"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Benchmark
func BenchmarkStringConcat(b *testing.B) {
a, c := "hello", "world"
for i := 0; i < b.N; i++ {
_ = a + " " + c
}
}
Memory Allocations
func BenchmarkStringBuilder(b *testing.B) {
a, c := "hello", "world"
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString(a)
sb.WriteString(" ")
sb.WriteString(c)
_ = sb.String()
}
}
// Run with: go test -bench=. -benchmem
Comparing Implementations
var testData = make([]int, 10000)
func init() {
for i := range testData { testData[i] = i }
}
func BenchmarkSumLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
var sum int
for _, v := range testData { sum += v }
_ = sum
}
}
func BenchmarkSumReduce(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Reduce(testData, 0, func(a, b int) int { return a + b })
}
}
Parallel Benchmark
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// Parallel operation
compute()
}
})
}
Sub-Benchmarks
func BenchmarkSort(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, size := range sizes {
b.Run(fmt.Sprintf("n=%d", size), func(b *testing.B) {
data := make([]int, size)
for i := 0; i < b.N; i++ {
b.StopTimer()
for j := range data { data[j] = size - j }
b.StartTimer()
sort.Ints(data)
}
})
}
}
Reset Timer
func BenchmarkWithSetup(b *testing.B) {
data := generateLargeData()
b.ResetTimer()
for i := 0; i < b.N; i++ {
process(data)
}
}
Common Mistakes
1. Compiler Optimizations
// Bad: result never used, compiler may eliminate
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ { add(3, 4) }
}
// Good: assign result to package-level variable
var result int
func BenchmarkAdd(b *testing.B) {
var r int
for i := 0; i < b.N; i++ { r = add(3, 4) }
result = r
}
2. Setup Inside Loop
Move setup code outside the benchmark loop or use b.ResetTimer.
3. Not Using benchstat
Run benchmarks multiple times: go test -bench=. -count=10 > old.txt. Compare with benchstat.
4. Forgetting -benchmem
Always benchmark with -benchmem to measure allocations and bytes per operation.
5. Starting Timer Too Early
Include setup time. Use b.ResetTimer() after setup. Use b.StopTimer/b.StartTimer for expensive setup.
Practice Questions
1. What does b.N control? The number of iterations. Go determines b.N automatically to get stable timing (usually 1ms+ of runtime).
2. How do you measure allocations? Use go test -bench=. -benchmem. Output shows allocs/op and bytes/op.
3. What is benchstat? A tool for statistical comparison of benchmark results. go install golang.org/x/perf/cmd/benchstat@latest.
4. Why assign results to a global variable? Prevents compiler from eliminating dead code. Without it, the benchmarked operation may be optimized away.
Challenge: Benchmark two implementations of JSON marshal: struct vs map.
Solution
type Data struct {
Name string `json:"name"`
Value int `json:"value"`
}
func BenchmarkMarshalStruct(b *testing.B) {
d := Data{Name: "test", Value: 42}
for i := 0; i < b.N; i++ {
json.Marshal(d)
}
}
func BenchmarkMarshalMap(b *testing.B) {
m := map[string]interface{}{"name": "test", "value": 42}
for i := 0; i < b.N; i++ {
json.Marshal(m)
}
}
FAQ
{{< faq question="How many iterations should I run?" >}}
Let Go decide with b.N. It automatically adjusts for stable timing. Use -benchtime=5s for longer runs.
{{< /faq >}}
{{< faq question="What is a good benchmark precision?" >}}
Run -count=10 and use benchstat. Benchmarks are noisy — multiple runs give statistical significance.
{{< /faq >}}
{{< faq question="How do I benchmark memory only?" >}}
Use -benchmem flag. Also try -bench=BenchmarkAlloc targeting allocation-heavy functions.
{{< /faq >}}
{{< faq question="Can I compare benchmarks across machines?" >}} Not meaningfully. Use relative comparison (before/after change) on the same machine. Normalize to operation count. {{< /faq >}}
{{< faq question="What is the difference between B and T?" >}} testing.B for benchmarks (measures performance). testing.T for tests (checks correctness). Both share common methods. {{< /faq >}}
Try It Yourself
package main
import "testing"
func BenchmarkHello(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "Hello, World!"
}
}
Run with:
go test -bench=. -benchmem
Expected output — benchmark result with ns/op, B/op, allocs/op.
What's Next
Now that you understand benchmarking, explore profiling for deep performance analysis.
| Topic | Description | Link |
|---|---|---|
| Go Profiling | Performance profiling | {{< ref "36-profiling" >}} |
| Go Testing Advanced | Advanced testing | {{< ref "34-testing-advanced" >}} |
| Go Modules | Package management | {{< ref "37-modules" >}} |