go原生模糊测试需同时满足四条件:fuzz开头大驼峰函数名、*testing.f参数、_test.go文件后缀、f.add()与f.fuzz()共存;f.fuzz仅支持string/[]byte/基本类型,结构体需拆解;种子须覆盖合法、非法、边界值。

Go 原生模糊测试不是“配好环境就能跑”,而是必须同时满足函数名、参数类型、文件后缀、种子注入这四个硬性条件,缺一不可;否则 go test -fuzz= 会静默跳过,连错误提示都不给。
为什么 go test -fuzz= 什么都没输出?
最常见原因是函数未被识别——不是报错,是直接忽略。检查以下四点:
-
Fuzz开头 + 大驼峰命名(FuzzParseJSON✅,fuzzParseJSON❌,Fuzzparsejson❌) - 参数类型必须是
*testing.F,不能是*testing.T或其他 - 文件名必须含
_test.go后缀,且不能是main.go或普通.go - 函数体里必须调用
f.Add()和f.Fuzz(),只写一个也不行
哪怕只漏掉 f.Add([]byte{}),fuzzer 也可能卡在 “no interesting inputs” 状态,因为它没拿到任何可变异的起点。
f.Fuzz() 闭包里能传什么类型?
只能是基础类型: string、[]byte、整数(int/int64)、浮点(float64)、布尔(bool)。不支持结构体、指针(除 *T 显式声明外)、切片嵌套或自定义类型。
例如想测 JSON 解析器,别写 f.Fuzz(func(t *testing.T, u User) { ... }) —— 编译失败。正确做法是:
- 用
string或[]byte接收原始输入,再手动json.Unmarshal - 若需结构体字段级变异,得自己拆解:比如对
timeout int字段,单独用f.Fuzz(func(t *testing.T, tmo int) { ... }) - 结构体指针可传(如
*Config),但必须用f.Add(&Config{...})注入,且确保字段类型本身在支持列表内
种子 f.Add() 怎么写才有效?
种子不是“随便塞几个值”,它是引导 fuzz 引擎进入关键路径的钥匙。无效种子会导致引擎长期在浅层逻辑打转。
- 合法典型值:
f.Add(`{"name":"alice"}`)—— 确保主流程能走通 - 非法格式:
f.Add(`{"name":`)(缺右括号)、f.Add([]byte{0x00, 0xff})—— 触发解析错误分支 - 边界值:
f.Add(strings.Repeat("x", 1(超长)、<code>f.Add(`{"arr":` + strings.Repeat("[],", 1000) + "[]}")(深度嵌套) - Unicode/控制字符:
f.Add([]byte(`{"name":"\u0000\u202e"}`))—— 暴露 UTF-8 校验或渲染漏洞
注意:f.Add("hello") 和 f.Add([]byte("hello")) 是两种不同类型的种子,不能混用;闭包参数是 string 就只能传 string,否则 panic。
崩溃复现不了?先盯住 goroutine 和 recover
原生 fuzz 只捕获主 goroutine 的 panic。如果被测逻辑启了新 goroutine 并在里面 panic,测试进程不会退出,crashers 目录也不会生成。
- 在
f.Fuzz()闭包开头加defer func() { if r := recover(); r != nil { panic(r) } }(),强制透出 - 避免在闭包里启动长期运行的服务(如
http.ListenAndServe),它会阻塞 fuzz 迭代;改用httptest.Server+ 超时 -
recover()成功捕获 panic 后,fuzzer 就“看不见”了——除非你主动panic(r),否则它不认为这是失败 -
os.Exit(1)、死锁、无限循环都不会触发 crash 保存,这类问题得换go-fuzz工具链
真正容易被忽略的是:fuzz 不验证业务逻辑是否“正确”,只管有没有 panic、崩溃或未覆盖路径。比如解析器返回了错误但没 panic,fuzzer 就当一切正常——你得自己在闭包里加 if err != nil && !isExpectedError(err) { t.Fatal(err) }。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











