Go fuzz测试必须严格满足三项:函数名以Fuzz开头、参数为*testing.F、文件名含_test.go且同包;否则直接跳过不报错;需用f.Add()注入种子并最小化崩溃输入。

func FuzzXxx(f *testing.F) 签名写错就完全不运行
Go 的 fuzz 测试函数不会报错、不会警告,只要签名不对,go test -fuzz=. 就直接跳过——你可能跑了一小时,实际什么都没测。最常见错误是写成 func TestFuzzXXX(t *testing.T) 或漏掉指针符号:func FuzzXXX(f testing.F)(少 *)。
必须严格满足三项:
- 函数名以
Fuzz开头(首字母大写) - 参数类型是
*testing.F(带星号) - 文件名是
*_test.go,且与被测代码同包
少一个条件,fuzzer 就当它不存在。建议写完立刻执行 go test -fuzz=FuzzXXX -fuzztime=1s 验证是否真被识别。
f.Fuzz() 回调里只能用 string、[]byte 等可序列化类型
不能传 struct{ Reader io.Reader },也不能传含 map、func、interface{} 的结构体。fuzzer 要把输入序列化后变异,不支持运行时不可确定的类型。
立即学习“go语言免费学习笔记(深入)”;
推荐做法:
- 单参数场景优先用
string或[]byte——覆盖文本解析、协议解包最直接 - 多参数组合时定义 flat struct:
type args struct{ Prompt string; MaxTokens int },但所有字段必须是基础类型或其数组/切片 - 避免在回调里做任何外部调用(HTTP、文件、
time.Now()),否则崩溃无法复现
种子(seed corpus)不靠 f.Add() 注入就等于没起点
fuzzer 不会自动读 testdata/ 下的文件,也不会看全局变量。没有 f.Add(),它就从纯随机字节开始变异,可能几十分钟都碰不到你关心的边界。
有效种子应包含:
- 典型合法输入:
f.Add("hello") - 已知易 crash 输入:
f.Add("\u0000")、f.Add(string([]byte{0xc0, 0x80}))(UTF-8 无效序列) - 业务特殊字符:
f.Add("\u200d")(Zero-Width Joiner)、f.Add("\ufeff")(BOM) - 超长/空输入:
f.Add(strings.Repeat("a", 10000))、f.Add("")
10 个高质量种子跑 30 秒,效果远胜盲目 fuzz 一小时。
崩溃输入自动存进 testdata/fuzz/FuzzXXX/,但得手动验证最小化
一旦触发 panic,fuzzer 会把导致崩溃的输入写进 testdata/fuzz/FuzzXXX/ 下的 .fuzz 文件,并在日志里打印路径。但这只是原始触发输入,往往包含大量冗余字节。
真正要修 bug 前,得手动做两件事:
- 用该
.fuzz文件内容调用你的函数,确认能稳定复现 - 尝试删减输入(比如逐字节删、用
go-fuzz-minimize工具),找到最短可复现 case —— 它才是你该放进单元测试的回归用例
很多人卡在“看到 panic 就停”,结果下次改代码又踩同一坑。最小化不是可选项,是修复闭环里最关键一步。


















