What is fuzzing in Go, and what kinds of bugs does it find?
Quick Answer
Fuzzing, built into the standard testing package since Go 1.18, automatically generates and mutates input values to a function under test, hunting for inputs that cause a crash, a panic, or a failed assertion — inputs a human writing example-based tests would likely never think to try. A fuzz test looks like func FuzzParse(f *testing.F) { f.Add("seed input"); f.Fuzz(func(t *testing.T, s string) { Parse(s) }) }, run with go test -fuzz=FuzzParse. It's especially effective for functions that parse untrusted input (like a custom format parser, a URL decoder, or anything handling user-supplied strings/bytes), since fuzzing systematically explores edge cases like empty strings, malformed UTF-8, and unexpected byte sequences that are easy to overlook when writing tests by hand.
Detailed Answer
Fuzzing finds a different class of bug than example-based tests: the inputs nobody thought to try.
A basic fuzz test
func FuzzParseCSVLine(f *testing.F) {
f.Add("a,b,c") // seed corpus: known-good starting inputs
f.Add("")
f.Add("a,,c")
f.Fuzz(func(t *testing.T, input string) {
result, err := ParseCSVLine(input)
if err != nil {
return // an error is a valid outcome, not a bug
}
// Whatever invariant should always hold if parsing succeeded:
if len(result) == 0 && input != "" {
t.Errorf("empty result for non-empty input %q", input)
}
})
}
go test -fuzz=FuzzParseCSVLine -fuzztime=30s
What the fuzzer actually does
Starting from the seed corpus (f.Add(...) calls), the fuzzer mutates inputs — flipping bytes, inserting characters, truncating strings, inserting known "interesting" values like empty strings or huge numbers — and runs the fuzz function against each mutation. Any input that causes a panic, an unrecoverable error, or a failed t.Errorf gets saved to a local corpus directory (testdata/fuzz/) as a permanent regression test.
The kind of bug it catches that example tests miss
func ParseCSVLine(s string) ([]string, error) {
parts := strings.Split(s, ",")
return parts[1:], nil // BUG: panics if there's only one field (parts has length 1)
}
A hand-written test with "a,b,c" would never trigger this, since it always has multiple fields. Fuzzing quickly discovers that a single-field input like "a" causes a slice-bounds panic, something easy to miss when writing example-based tests by hand.
Where fuzzing earns its keep
Anything parsing untrusted or externally-supplied input — file formats, network protocols, user-submitted strings — is exactly where fuzzing tends to find real bugs. It explores the input space far more broadly than a human would think to, which is why careful manual test-writing still misses these cases.