Skip to content

Intermediate · Packages, testing and generics · topic 24 of 46 · 30 min

Benchmarks and fuzz tests

Explanation

A benchmark, BenchmarkXxx(b *testing.B), measures how fast code runs; go test -bench=. runs them and -benchmem adds memory use. Measure before you optimise. A fuzz test, FuzzXxx(f *testing.F), feeds your function many generated inputs to find crashes and broken rules you did not think of.

Measure, do not guess

Developers often guess wrongly about what is slow. A benchmark runs a piece of code many times and reports the average time per run. Go's testing package has benchmarks built in, so you can compare two versions of a function with real numbers.

go
// in a _test.go file
func BenchmarkJoinPlus(b *testing.B) {
	for i := 0; i < b.N; i++ {
		s := ""
		for j := 0; j < 100; j++ {
			s += "x"
		}
	}
}

func BenchmarkJoinBuilder(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var sb strings.Builder
		for j := 0; j < 100; j++ {
			sb.WriteString("x")
		}
		_ = sb.String()
	}
}
b.N is chosen by the framework: it raises it until the timing is reliable.
bash
go test -bench=. -benchmem
# BenchmarkJoinPlus-8       <runs>   <time> ns/op   <bytes> B/op   <many> allocs/op
# BenchmarkJoinBuilder-8    <runs>   <time> ns/op   <bytes> B/op   <few> allocs/op
go test -run='^$' -bench=Builder   # skip normal tests, only matching benchmarks
ns/op is time per operation; B/op and allocs/op are memory per operation. The + version allocates a new string on almost every step, the Builder only when its buffer grows. The -8 is the GOMAXPROCS value. Your numbers will differ.

The newer b.Loop style (Go 1.24+)

Go 1.24 added b.Loop(). You write for b.Loop() { ... } instead of the b.N loop. It handles timing set-up for you and stops the compiler from optimising away the work being measured. Older code and older Go versions use the b.N loop, so you should recognise both.

go
func BenchmarkWithGST(b *testing.B) {
	for b.Loop() {
		WithGST(10000)
	}
}
Requires Go 1.24 or newer.

Reading benchmark results honestly

  • Run on a quiet machine; other programs add noise.
  • Run several times (-count=10) and compare with the benchstat tool (golang.org/x/perf/cmd/benchstat) rather than one run.
  • Benchmark realistic input sizes. Code that is fast for 10 items may be slow for 10 lakh.
  • A function that is 2x faster but runs once a day does not matter. Optimise what profiling shows is hot (the pprof profiler is the next step).

Fuzzing: let the computer find your bugs

Your table tests contain cases you thought of. Fuzzing (added in Go 1.18) generates thousands of inputs you did not think of: odd Unicode, empty strings, huge numbers. You give a few seed inputs and a property that must always hold. When the fuzzer finds an input that fails or panics, it saves it under testdata/fuzz/ so it becomes a permanent regression test.

go
func Reverse(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}

func FuzzReverse(f *testing.F) {
	f.Add("hello")   // seed corpus
	f.Add("नमस्ते")
	f.Fuzz(func(t *testing.T, s string) {
		twice := Reverse(Reverse(s))
		if utf8.ValidString(s) && twice != s {
			t.Errorf("Reverse twice changed %q into %q", s, twice)
		}
	})
}
The property: reversing twice gives back the original (for valid UTF-8).
bash
go test                         # runs seeds only, like a normal test
go test -fuzz=FuzzReverse -fuzztime=30s   # generate new inputs for 30 seconds
Without -fuzz, the seeds and saved failures run as ordinary tests, so CI stays fast.
Careful: Fuzzing uses a lot of CPU and can run forever. Always set -fuzztime when running it by hand, and fuzz one target at a time.
Tip: Good fuzz targets are parsers and validators: PIN code checkers, invoice number parsers, JSON or CSV readers, anything that takes outside input.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Benchmarks and fuzz tests · 1/7

Benchmarks and fuzzing

Before you optimise, measure. Go has benchmarks built into go test.

Prefer a video by a teacher? Find free videos on “golang benchmark testing.B benchmem and go fuzzing tutorial” on YouTube ↗

In real life: Speeding up invoice number formatting

A billing service formats lakhs of invoice numbers per day. A developer suspects fmt.Sprintf is slow and writes a version with strconv. Instead of guessing, two benchmarks with -benchmem show the difference in ns/op and allocations, and a fuzz test checks that both versions always give the same text for any number.

go
func FormatSprintf(n int) string { return fmt.Sprintf("INV-%06d", n) }

func FormatStrconv(n int) string {
	s := strconv.Itoa(n)
	if len(s) < 6 {
		s = strings.Repeat("0", 6-len(s)) + s
	}
	return "INV-" + s
}

func FuzzFormatSame(f *testing.F) {
	f.Add(42)
	f.Fuzz(func(t *testing.T, n int) {
		if n < 0 {
			t.Skip()
		}
		if a, b := FormatSprintf(n), FormatStrconv(n); a != b {
			t.Errorf("n=%d: %q != %q", n, a, b)
		}
	})
}
Benchmarks say which is faster; the fuzz test proves they agree for non-negative numbers.

Practice (0/4 done)

Task 1 · Question

Which command runs all benchmarks and shows memory allocations?

Task 2 · Question

In a classic benchmark, who decides the value of b.N?

Task 3 · Question

Where does the fuzzer save an input that made the test fail?

Task 4 · Practice

Write two functions that sum a []int: one with a for range loop and one with a classic index loop. Benchmark both with -benchmem on a slice of 10,000 numbers built once outside the loop. Then write a fuzz test checking that the two always return the same sum.

Quick quiz

Question 1

1.What does ns/op mean in benchmark output?

Question 2

2.Which Go version added fuzzing to go test?

Question 3

3.Without the -fuzz flag, what does go test do with a Fuzz function?

Question 4

4.What is a good fuzz target?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does ns/op mean in benchmark output?

Answer: Nanoseconds per operation

It is the average time for one iteration.

Which Go version added fuzzing to go test?

Answer: 1.18

Native fuzzing arrived in Go 1.18, together with generics.

Without the -fuzz flag, what does go test do with a Fuzz function?

Answer: Runs it with the seed corpus and saved inputs only

Seeds and saved failures run as normal tests.

What is a good fuzz target?

Answer: A parser or validator that handles outside input

Code that processes untrusted input benefits most from fuzzing.

A loop over n items that calls array.includes on another n items is…Related

Answer: O(n²)

includes is itself a loop, so the total is n × n.

From Performance thinking and Big-O

What does go test -cover report?Related

Answer: Percentage of statements run by the tests

Coverage is the share of statements executed during the tests.

From Testing with go test

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Packages, testing and generics” before its exam: Generics: type parameters and constraints.
  2. Module 5 exam: Packages, testing and generics · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Concurrency: goroutines, channels and context. 46 topics left in the course before the final exam.

← Previous: Testing with go test