Go ships fuzzing in its standard test tooling. You write a function named FuzzSomething, add a few seed inputs, and run go test with the fuzz flag. When an input fails, Go saves it under testdata so it becomes a permanent regression test. That is the whole loop.
Write the fuzz target
A fuzz test takes a *testing.F. You add seed inputs with f.Add, then call f.Fuzz with a function that receives the generated arguments. The Go documentation says there must be exactly one fuzz target per fuzz test, and the argument types must match the seed entries in order.
func FuzzParseHeader(f *testing.F) {
f.Add("Content-Length: 12")
f.Add("X-Empty:")
f.Fuzz(func(t *testing.T, line string) {
h, err := ParseHeader(line)
if err != nil {
return
}
// property: a parsed header can be written back and re-parsed
again, err := ParseHeader(h.String())
if err != nil || again != h {
t.Fatalf("round trip failed for %q", line)
}
})
}
The check inside is a property. A bare "does not panic" is a perfectly good start, and a round trip catches logic bugs as well as crashes.
Run it two ways
Run plainly, go test treats the fuzz test like a unit test and only replays the seed entries. That mode is what your normal CI should run.
To fuzz, pass the flag.
go test -fuzz=FuzzParseHeader -fuzztime=60s
Go gathers baseline coverage from the existing corpus first, then generates new inputs and keeps those that reach new code. The docs list settings such as the total fuzz time, the minimization time and the number of parallel workers.
What happens on failure
When an input causes a failure, the engine tries to minimize it to the smallest value that still fails. The failing input is written to a file under testdata/fuzz/FuzzParseHeader/, and the output prints the exact command to re-run it. Commit that file. From then on, a plain go test replays it, so the bug cannot return unnoticed.
Rules for a good fuzz target
The Go guidance is practical. Make the target fast, because speed means more inputs per second. Keep it deterministic. The docs state that the target is called in parallel and in nondeterministic order, so state must not persist across calls and behavior should not depend on global state.
Avoid network calls and disk writes inside the target. If you need a fixture, build it in memory.
Where fuzzing fits
Use it on code that parses untrusted bytes: headers, archives, config formats, image metadata, protocol frames. Skip it for code that is only business rules, where property tests or table tests are a better fit.
Quick check
Do I run the fuzz flag on every pull request?
Usually not for long. Replay the saved corpus on every pull request, and give the fuzzing itself a scheduled slot, such as a nightly job with a time limit.
Sources
Last reviewed: 19 October 2026.