Go fuzz test函数必须接收testing.F参数,签名唯一为func FuzzXxx(f testing.F),需以Fuzz开头,使用f.Add()注入种子、f.Fuzz()定义逻辑,回调中仅支持基本或可序列化类型,校验须在回调内完成。

Go fuzz test 函数必须接收 *testing.F 参数
Go 原生 fuzzing 不是普通测试函数,不能写成 func TestXxx(t *testing.T)。一旦用错参数类型,go test -fuzz=. 会直接跳过该函数,且不报错、不提示——这是最常被卡住的点。
正确签名只有一种:func FuzzXxx(f *testing.F)。注意是 Fuzz 开头(首字母大写),且参数必须是 *testing.F。
- 函数名必须以
Fuzz开头,否则go test不识别为 fuzz target -
*testing.F提供f.Add()(注入种子)、f.Fuzz()(定义模糊逻辑)等方法,没有f.Error或f.Log等*testing.T的方法 - 不能在
f.Fuzz()回调外执行断言或 panic;所有校验必须放在回调函数体内
f.Fuzz() 回调里只能接收基本类型或可序列化类型
Go fuzz engine 通过序列化输入生成变异数据,因此 f.Fuzz() 的回调函数参数类型受限。传入 struct{ io.Reader } 或 func() 会编译失败,错误信息是 cannot fuzz type xxx。
支持的类型包括:string、[]byte、int/int64、float64、bool,以及由这些类型组成的 flat struct(字段不能含指针、channel、func、interface{}、map、slice of struct 等)。
- 推荐首选
string或[]byte:覆盖文本解析、协议解包、正则匹配等场景最直接 - 若需多参数组合,定义简单 struct,例如:
type args struct{ a int; b string },但确保所有字段可 fuzz - 避免在回调里做耗时操作(如 HTTP 调用、文件读写),fuzz 运行时可能执行数万次,超时会导致测试中断
seed corpus 必须用 f.Add() 显式注入,不能靠文件或全局变量
Go fuzz 不自动加载 testdata/ 下的样本,也不会读取全局变量。所有初始种子(seed corpus)必须在 FuzzXxx 函数开头用 f.Add() 注册,否则 fuzz 引擎从纯随机起点开始,可能很久才覆盖关键边界。
例如验证 JSON 解析器,应主动添加 "{}"、"{\"a\":1}"、"\u0000"、string([]byte{0xff, 0xfe}) 这类易触发 panic 的输入。
-
f.Add()可多次调用,每次传入一组对应f.Fuzz()参数类型的值 - 字符串种子尽量包含控制字符、UTF-8 边界序列(如
"\xc0\x80")、超长重复字符等 - 不要依赖
init()或包级变量预设状态;fuzz 每次运行都是干净的 goroutine
崩溃复现依赖 failing input 日志,而非堆栈本身
当 fuzz 找到导致 panic 或失败的输入时,Go 测试框架会输出类似 failing input: []byte("...") 的十六进制或字符串表示,并保存到 testdata/fuzz/FuzzParse/ 下的 seed 文件中。但这个路径不会自动加入 git,也不参与常规 go test 运行。
真正关键的是把失败输入转成普通 TestXxx 复现用例——否则下次 CI 里没人知道问题是否回归。
- 复制日志中的
failing input,写死进一个TestXxx函数里,直接调用被测逻辑并 assert - 别只截图或记在文档里;fuzz 发现的 bug 很可能是内存越界或竞态,必须能稳定复现
- 如果
f.Fuzz()里用了外部状态(如全局 map),记得在每次回调开始前重置,否则不同输入相互污染,失败不可重现
Go fuzz 的威力不在“跑得久”,而在“跑得准”——它依赖你提供的种子质量和回调里校验逻辑的严密性。少一个 f.Add(),可能漏掉整个 Unicode 边界类问题;多一个未清理的全局变量,会让 fuzz 结果变成玄学。

















