A benchmark in Go is an automated test that measures how long a piece of code takes to execute.
It is not for proving that something works. For that we have tests. A benchmark answers a different question: how much does this cost?
Go includes benchmark support in the same testing package, so we can measure performance without installing external tools.
Creating a Benchmark
Suppose we have this function:
package texto
func Repetir(s string, n int) string {
resultado := ""
for i := 0; i < n; i++ {
resultado += s
}
return resultado
}We create a file texto_test.go:
package texto
import "testing"
func BenchmarkRepetir(b *testing.B) {
for b.Loop() {
Repetir("go", 100)
}
}The function starts with Benchmark and receives b *testing.B.
The Benchmark Loop
Since Go 1.24, the recommended approach is to use b.Loop().
for b.Loop() {
// Code we want to measure
}Go runs the loop as many times as needed to reach the measurement time. b.Loop() automatically excludes setup and cleanup code placed outside the loop, prevents relying on the current iteration, and keeps the arguments and return values of calls within the body alive.
You will also see many older examples using b.N:
for i := 0; i < b.N; i++ {
// Code we want to measure
}That pattern still works, but for new code it’s better to use b.Loop().
Running Benchmarks
We use:
go test -bench=.Typical output:
BenchmarkRepetir-12 120000 9800 ns/op
PASSThe final number (ns/op) indicates nanoseconds per operation. Lower is better, as long as we are measuring the right thing.
Preventing the Compiler from Eliminating Work
In a classic benchmark, if the result is not used, the compiler might eliminate some of the work.
With b.Loop(), arguments, return values, and variables assigned inside the body are kept alive, as long as the condition is written exactly as b.Loop(). Therefore, we don’t need a global variable just to prevent result elimination:
func BenchmarkRepetir(b *testing.B) {
for b.Loop() {
Repetir("go", 100)
}
}Setup Inside and Outside the Loop
Many times we need to prepare data before measuring.
func BenchmarkProcesar(b *testing.B) {
datos := cargarDatosDePrueba()
for b.Loop() {
Procesar(datos)
}
}With b.Loop(), the preceding setup is excluded from the measurement. If we need to prepare data in each iteration, we can wrap that part with b.StopTimer() and b.StartTimer(). b.ResetTimer() still appears in classic benchmarks with b.N, but is not needed in this example.
Measuring Memory Allocations
To see allocations:
go test -bench=. -benchmemOutput:
BenchmarkRepetir-12 120000 9800 ns/op 5300 B/op 99 allocs/opThis tells us:
B/op: bytes allocated per operation.allocs/op: number of allocations per operation.
In Go, many interesting optimizations involve reducing allocations, not just saving nanoseconds.
Comparing Two Implementations
We can write another benchmark using strings.Builder, which is more suitable for concatenating many pieces.
func RepetirBuilder(s string, n int) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(s)
}
return builder.String()
}
func BenchmarkRepetirBuilder(b *testing.B) {
for b.Loop() {
RepetirBuilder("go", 100)
}
}Now we run:
go test -bench=. -benchmemA single run can be affected by CPU frequency, temperature, and system processes. For a serious comparison, we run multiple samples and use benchstat:
go test -run '^$' -bench=. -benchmem -count=10 > resultados.txt
benchstat resultados.txtbenchstat is part of golang.org/x/perf/cmd/benchstat. The important thing is not that one version appears faster, but having repeated data for our specific case.