Skip to main content

Why the testing Package?

Go has first-class benchmarking built into its standard library — no external framework needed. The testing package provides testing.B, which handles iteration control, timing, memory tracking, sub-benchmarks, and parallel execution out of the box. Every Go developer already has the tools installed. Benchmarks live in _test.go files alongside your code, run with go test, and integrate with the Go ecosystem’s profiling tools (pprof, benchstat).

Your First Benchmark

Let’s start with the simplest possible Go benchmark: measuring a recursive Fibonacci function.

Setting Up

Create a module and two files — the function and its benchmark:
terminal
fib.go
fib_test.go
A few things to note:
  • Benchmark functions must start with Benchmark and accept *testing.B.
  • b.Loop() (Go 1.24+) controls iteration. Iteration count and timing are handled automatically.
  • The function lives in a _test.go file, just like unit tests.
b.Loop() was introduced in Go 1.24. It replaces the older for i := 0; i < b.N; i++ pattern and is more precise — it automatically resets the timer, and the compiler is prevented from optimizing away the loop body. If you are on an older Go version, use the b.N pattern instead:
Legacy pattern (before Go 1.24)

Running the benchmarks

terminal
The highlighted line breaks down as follows:
The framework automatically adjusts the iteration count to run for at least 1 second by default. The -8 suffix on BenchmarkFibonacci-8 is the GOMAXPROCS value, defaulting to the number of available CPUs.

Configuring Your Benchmarks

Benchmark Duration

Control how long each benchmark runs with -benchtime:
terminal
You can also specify an exact iteration count:
terminal

Memory Allocation Tracking

Add -benchmem to report allocation stats, or call b.ReportAllocs() inside the benchmark:
terminal
terminal
The two extra columns show bytes allocated per operation and number of allocations per operation. These are essential for catching allocation regressions — even if latency stays flat, increased allocations put pressure on the garbage collector.

Filtering and Skipping Tests

Run only benchmarks (skip unit tests) with a regex:
terminal
Filter to specific benchmarks:
terminal
Run benchmarks in a specific package, or recursively across all packages:
terminal

Key CLI Flags

-bench
regexp
required
Run benchmarks matching the regular expression. Use -bench=. for all.
-benchtime
duration
default:"1s"
Minimum time per benchmark. Accepts a duration (5s, 100ms) or an exact iteration count (1000x).
-benchmem
bool
default:"false"
Report memory allocation statistics (B/op, allocs/op).
-count
int
default:"1"
Run each benchmark n times. Use -count=10 or higher for statistical analysis with benchstat.
-cpu
list
Comma-separated GOMAXPROCS values to test with (e.g., -cpu=1,2,4,8).
-run
regexp
Filter tests. Use -run='^$' to skip unit tests when benchmarking.
-timeout
duration
default:"10m"
Maximum total time for all tests and benchmarks.

Sub-benchmarks and Table-Driven Patterns

Sub-benchmarks with b.Run

Use b.Run() to create sub-benchmarks — the standard way to test different inputs or configurations:
fib_test.go
terminal
The exponential growth of recursive Fibonacci is clearly visible: n=5 takes 21ns, n=30 takes 3.8ms — a factor of 180,000x. You can filter sub-benchmarks from the command line:
terminal

Comparing Algorithms

Sub-benchmarks make algorithm comparison straightforward:
fib_test.go
terminal
At n=30, the iterative version is 377,000x faster than the recursive one (10ns vs 3.8ms). The hierarchical sub-benchmark naming makes it easy to compare across both dimensions.

Benchmarking Only What Matters

Excluding Setup with b.ResetTimer

When your benchmark has expensive one-time setup, use b.ResetTimer() to exclude it from measurements:
Excluding setup
With b.Loop() (Go 1.24+), the timer is automatically reset on the first iteration, so b.ResetTimer() is no longer required unless the setup happens inside the loop body.

Per-Iteration Setup with Timer Control

When each iteration needs fresh data (e.g., sorting an unsorted slice), use b.StopTimer() and b.StartTimer():
sort_test.go
terminal
b.StopTimer() and b.StartTimer() have overhead. If the per-iteration setup is extremely cheap relative to what you are measuring, the timer overhead may distort results. Use this pattern only when the setup cost is significant.

Custom Metrics

Report domain-specific metrics with b.ReportMetric():
Custom metrics
Use b.SetBytes(n) to report throughput in MB/s for I/O-bound benchmarks:
Throughput reporting

Parallel Benchmarks

Use b.RunParallel() to benchmark code under concurrent load. This creates GOMAXPROCS goroutines and distributes iterations among them:
fib_test.go
terminal
Compare this with the sequential result (31076 ns/op) — the parallel version runs ~5x faster on 8 cores, showing that this CPU-bound workload scales well across threads.
Do not call b.StopTimer(), b.StartTimer(), or b.ResetTimer() inside b.RunParallel. They have global effect and will corrupt measurements. Each goroutine must maintain its own local state.
Use b.SetParallelism(n) to increase concurrency beyond GOMAXPROCS for I/O-bound workloads:
High concurrency

Avoiding Common Pitfalls

Compiler Dead Code Elimination

If a computation’s result is unused, the Go compiler may eliminate it entirely. This is the single most common source of misleading benchmark results.
Dead code elimination
If you must use the b.N pattern (Go < 1.24), pass the result to runtime.KeepAlive so the compiler treats it as observed:
runtime.KeepAlive pattern

Do Not Use b.N as Input

Using b.N as a function parameter means the workload grows with the iteration count. The benchmark never converges and reports meaningless numbers:
b.N misuse

Keep Benchmarks Deterministic

Use fixed seeds for random data:
Deterministic setup

Comparing Results with benchstat

Raw benchmark numbers are noisy. Use benchstat to compare results with statistical rigor.

Installation

terminal

Workflow

Run benchmarks multiple times (at least 10) to collect enough samples:
terminal
Make your changes, then run again:
terminal
Compare with benchstat:
terminal
The columns report:
  • ± 0%: the 95% confidence interval. Lower means more stable results.
  • ~ (p=0.841): no statistically significant difference. The p-value is from a Mann-Whitney U-test; values below 0.05 indicate a real change.
  • n=10: the sample count. Use -count=10 or higher for reliable results.

Profiling with pprof

Go benchmarks integrate directly with the pprof profiler. Generate profiles while benchmarking:
-cpuprofile
file
Write CPU profile. Shows where time is spent.
-memprofile
file
Write memory allocation profile. Shows where allocations happen.
-blockprofile
file
Write goroutine blocking profile. Shows where goroutines wait.
-mutexprofile
file
Write mutex contention profile. Shows lock contention hotspots.
Generate and analyze a CPU profile:
terminal
This opens an interactive web UI with flamegraphs, call graphs, and source-level annotations.
Collect only one profile type at a time for accuracy — profiling itself has overhead that can distort other measurements.

Best Practices

Run on Idle Machines

Close background processes, avoid running on battery, and disable CPU throttling when collecting benchmark data. Noise from other processes can mask real performance changes.

Use -count and benchstat for Decisions

Never eyeball raw ns/op numbers to decide if a change helped. Run with -count=10 and use benchstat to test for statistical significance. With ~20 benchmarks at alpha=0.05, expect ~1 false positive.

Track Memory Alongside Latency

Always use -benchmem or b.ReportAllocs(). Even if latency stays flat, increased allocations put pressure on the garbage collector and cause latency spikes under production load.

Use Sub-Benchmarks for Input Variation

Table-driven sub-benchmarks let you test across input sizes, data shapes, and configurations in a single benchmark function. They also enable filtering from the command line.

Running Benchmarks Continuously with CodSpeed

So far, you’ve been running benchmarks locally. But local benchmarking has limitations:
  • Inconsistent hardware: Different developers get different results
  • Manual process: Easy to forget to run benchmarks before merging
  • No historical tracking: Hard to spot gradual performance degradation
  • No PR context: Can’t see performance impact during code review
This is where CodSpeed comes in. It runs your benchmarks automatically in CI and provides:
  • Automated performance regression detection in PRs
  • Consistent metrics with reliable measurements across all runs
  • Historical tracking to see performance over time with detailed charts
  • Flamegraph profiles to see exactly what changed in your code’s execution
For the full CodSpeed integration reference, see Writing Benchmarks in Go.

How to Set Up CodSpeed

Here’s how to integrate CodSpeed with your Go benchmarks:
1

Set Up GitHub Actions

Create a workflow file to run benchmarks on every push and pull request.
2

Check the Results

Once the workflow runs, your pull requests will receive a performance report comment:Pull Request ResultPull Request Result
3

Access Detailed Reports and Flamegraphs

After your benchmarks run in CI, head over to your CodSpeed dashboard to see detailed performance reports, historical trends, and flamegraph profiles for deeper analysis.
Profiling Report on CodSpeed

Profiling Report on CodSpeed

Profiling works out of the box, no extra configuration needed!Learn more about flamegraphs and how to use them to optimize your code.

Next Steps

Check out these resources to continue your Go benchmarking journey:

Get Started with CodSpeed

Sign up and start tracking your Go performance in CI

CodSpeed Go Benchmarking Docs

CodSpeed’s Go integration reference and compatibility notes

Benchmarking a Go Gin API

A hands-on guide to benchmarking a real HTTP API with Gin

Performance Profiling

Learn how to use flamegraphs to optimize your code