What is fuzzing in Go, and what kinds of bugs does it find?

5 minadvancedgotestingfuzzing

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.

Related Resources