How do you write and run benchmarks in Go?

5 minintermediategotestingbenchmarks

Quick Answer

A benchmark function has the signature func BenchmarkXxx(b *testing.B), and its body runs a loop for i := 0; i < b.N; i++ { ... } around the code being measured. The testing framework calls this function repeatedly, automatically adjusting b.N until it gets a stable, statistically meaningful timing. Run benchmarks with go test -bench=<pattern> (benchmarks aren't run by default with plain go test), and add -benchmem to also report allocations per operation, which often matters as much as raw speed. b.ResetTimer() lets you exclude expensive setup code from the timed portion, and b.Run (like t.Run) lets you organize multiple related benchmark variants under one parent function.

Detailed Answer

Benchmarks in Go are just another kind of test function, following a specific naming and signature convention the tooling recognizes automatically.

A basic benchmark

func BenchmarkFibonacci(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Fibonacci(20)
    }
}
go test -bench=Fibonacci -benchmem
BenchmarkFibonacci-8    5000000    287 ns/op    0 B/op    0 allocs/op

b.N isn't a number you choose — the framework runs the loop with increasing b.N values until the total run time is long enough to measure reliably, then reports the stable per-iteration cost.

Excluding setup cost with ResetTimer

func BenchmarkProcessLargeFile(b *testing.B) {
    data := loadLargeTestFixture()  // expensive setup, shouldn't count toward the benchmark
    b.ResetTimer()                  // timer restarts here

    for i := 0; i < b.N; i++ {
        Process(data)
    }
}

Without b.ResetTimer(), the one-time cost of loadLargeTestFixture() would be included in the measurement, skewing the result, especially since it only happens once but b.N might be in the millions.

Comparing implementations with sub-benchmarks

func BenchmarkConcat(b *testing.B) {
    b.Run("plus-operator", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = "a" + "b" + "c"
        }
    })
    b.Run("strings-builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            sb.WriteString("a")
            sb.WriteString("b")
            sb.WriteString("c")
            _ = sb.String()
        }
    })
}
go test -bench=BenchmarkConcat

This runs both variants under the same parent benchmark, letting you directly compare their ns/op output to see which approach is actually faster for your specific case, rather than assuming.

Why benchmarks aren't run by default

go test          # runs only Test* functions
go test -bench=. # also runs Benchmark* functions matching "."(every benchmark)

Benchmarks are typically slower and noisier to run than unit tests, so they're opt-in via the -bench flag rather than part of every normal go test invocation.