Safeguard
Developer Tools•

Go native fuzzing: write and run your first fuzz test

Go has fuzzing built into go test. Write a FuzzXxx function, add seeds, run with -fuzz, and keep failing inputs as regression tests. A short walkthrough.

Safeguard Security Team
3 min read

Go has fuzzing in the standard toolchain from Go 1.18. You write a function named FuzzSomething that takes a testing.F, add a few seed values, and run go test with the fuzz flag. When it finds a failure, Go saves the input in your repo so it becomes a regression test.

The shape of a fuzz test

From the Go documentation: a fuzz test is a function named like FuzzXxx, accepting only a testing.F, with no return value, in a file ending in _test.go. Inside it you call Fuzz once, with a function whose first parameter is a testing.T followed by the values to fuzz.

func FuzzReverse(f *testing.F) {
    f.Add("hello")
    f.Fuzz(func(t *testing.T, s string) {
        r := Reverse(s)
        if Reverse(r) != s {
            t.Errorf("round trip failed for %q", s)
        }
    })
}

Seed values added with f.Add must match the argument types in order. Supported types include string, byte slices, the integer types, floats and bool.

Test properties, not just crashes

This example does not wait for a panic. It checks a property: reversing twice gives back the original. Go reports a failure for panics, calls to t.Fail, t.Error or t.Fatal, unrecoverable errors, and an execution that takes too long. The docs state the timeout for one execution of a fuzz target is currently 1 second.

Round trips, comparing two implementations, and "output is valid" checks all turn a fuzzer into a logic bug finder, not only a crash finder.

Run it

Plain go test runs each seed as an ordinary unit test. To fuzz, name the test:

go test -fuzz=FuzzReverse -fuzztime=30s

The flag takes a regex that matches a single fuzz test. Without -fuzztime, fuzzing can run indefinitely if no error is found. The output shows elapsed time, executions, and a count of new interesting inputs, which are the ones that reached new coverage.

What happens on failure

The engine minimizes the failing input and writes it to testdata/fuzz/FuzzReverse/ under a hash name. Rerun just that case with go test -run=FuzzReverse/<hash>. Once the bug is fixed, the file stays in place, and per the docs it is run by default with go test as a regression test. Commit it.

Corpus files begin with a version line, go test fuzz v1, followed by one line per argument value. Seed files you place in testdata/fuzz/FuzzName are also run by default.

Tips from the docs

  • Keep the target fast and deterministic.
  • Do not depend on global state, because the target runs in parallel in nondeterministic order.
  • Expect coverage guidance to work best on AMD64 and ARM64.
  • Use -parallel to set the number of fuzzing processes, which defaults to GOMAXPROCS.

Where it fits in CI

Plain go test in CI replays seeds and saved failures, which is cheap and catches regressions. A longer fuzz run, say nightly, hunts for new bugs. Commit anything it finds.

Quick check

Does go test run my fuzz test forever?

No. Without the fuzz flag it only runs the seed corpus as unit tests. Fuzzing starts only when you pass -fuzz.

Where does Go store the generated corpus?

Generated entries live in the build cache under a fuzz directory, while seed files and failing inputs live in testdata/fuzz in your repository.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.