Intermediate · Packages, testing and generics · topic 24 of 46 · 30 min
Benchmarks and fuzz tests
Explanation
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.
// 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()
}
}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 benchmarksThe 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.
func BenchmarkWithGST(b *testing.B) {
for b.Loop() {
WithGST(10000)
}
}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.
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)
}
})
}go test # runs seeds only, like a normal test
go test -fuzz=FuzzReverse -fuzztime=30s # generate new inputs for 30 secondsNeed 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
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.
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)
}
})
}Practice (0/4 done)
Which command runs all benchmarks and shows memory allocations?
In a classic benchmark, who decides the value of b.N?
Where does the fuzzer save an input that made the test fail?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
Questions people ask about this
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
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 more topic in “Packages, testing and generics” before its exam: Generics: type parameters and constraints.
- Module 5 exam: Packages, testing and generics · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Concurrency: goroutines, channels and context. 46 topics left in the course before the final exam.