Go Fuzz 怎样增加解析器模糊测试
给解析器增加 Go Fuzz,核心不是随机拼几段字符串,而是把“输入不能让程序崩溃”和“成功结果必须满足的约束”写成测试。一个实用的起点是让 FuzzXxx(*testing.F) 接收 []byte,用 f.Add 放入正常、空值和边界样例,再在 f.Fuzz 中调用解析器。解析失败可以是合法分支;真正需要阻止的是 panic、死循环和成功结果违反基本契约。
Go 官方地址:https://go.dev/doc/tutorial/fuzz。原生模糊测试从 Go 1.18 起进入标准工具链,testing.F 负责种子语料和 fuzz target,生成的失败输入还可以回到后续普通测试中。
先准备一个有边界的键值解析器
下面用 key=value;key=value 表示一组简单配置。解析器允许空输入,忽略末尾多余的分号;缺少等号、键为空或键重复时返回错误。这个契约足够小,读者可以把它换成自己的协议头、查询参数或配置片段。
package parser
import (
"fmt"
"strings"
)
// ParsePairs 解析分号分隔的键值对,并拒绝空键和重复键。
func ParsePairs(input []byte) (map[string]string, error) {
result := make(map[string]string)
for _, field := range strings.Split(string(input), ";") {
if field == "" { // 允许空输入以及末尾分隔符。
continue
}
key, value, ok := strings.Cut(field, "=")
if !ok || key == "" { // 缺少等号或键为空都不是合法记录。
return nil, fmt.Errorf("invalid pair %q", field)
}
if _, exists := result[key]; exists { // 重复键会让结果含义不明确。
return nil, fmt.Errorf("duplicate key %q", key)
}
result[key] = value // 只切第一处等号,值可以继续包含等号。
}
return result, nil
}
这里的函数没有把任何字节都当成错误,也没有把错误当成异常。模糊测试时,随机输入很可能落入错误分支;只要函数稳定返回,就已经覆盖了边界。真正要检查的是成功返回的 map 不为 nil、键不为空,并且不会出现分隔符残留在键中。

用 f.Add 先放进有价值的种子
种子不需要覆盖所有组合,但应该覆盖业务最常见和最容易漏掉的形态:正常键值、空输入、只有分隔符、缺少等号、空键,以及值中含有等号。f.Add 的参数类型必须和后面 fuzz target 的参数完全一致。
package parser
import (
"strings"
"testing"
)
func FuzzParsePairs(f *testing.F) {
f.Add([]byte("name=go;mode=test")) // 正常的两个键值对。
f.Add([]byte("")) // 空输入应得到空结果。
f.Add([]byte(";;;")) // 只有分隔符,不应触发 panic。
f.Add([]byte("broken")) // 缺少等号,覆盖错误返回。
f.Add([]byte("=empty-key")) // 空键,覆盖另一条错误分支。
f.Add([]byte("query=a=b")) // 值里还有等号,确认只切第一处。
f.Fuzz(func(t *testing.T, input []byte) {
got, err := ParsePairs(input)
if err != nil { // 任意格式错误都可以返回,不代表 fuzz 失败。
return
}
if got == nil { // 成功时必须返回可用的 map。
t.Fatal("successful parse returned nil map")
}
for key, value := range got {
if key == "" || strings.ContainsAny(key, ";=") {
t.Fatalf("invalid key %q from %q", key, input)
}
_ = value // 值允许包含等号,当前契约不限制其内容。
}
})
}
当只运行普通 go test 时,种子会先被执行;打开 -fuzz 后,工具链会在种子基础上变异输入,并用覆盖率变化判断哪些输入值得保留。种子越贴近解析器的关键分支,模糊测试越容易较早触达有意义的区域。
先跑种子,再用 -fuzztime 控制实验长度
开发阶段建议先用普通测试确认种子本身没有写错,再给 fuzz target 一个明确的时间预算。下面的命令只展示运行方式,输出中的执行次数会随机器和 Go 版本变化,不应写死在文档里。
# 先执行普通测试,确认所有种子都能进入 fuzz target。 go test ./... # 只运行 FuzzParsePairs,跳过其他普通测试,并限制实验时长。 go test -run=^$ -fuzz=FuzzParsePairs -fuzztime=30s
-fuzz=FuzzParsePairs 选择要持续生成输入的目标;-fuzztime=30s 让本地实验可重复结束。没有设置时间限制时,模糊测试可能一直运行,直到遇到失败或被手动中断。Go 官方文档还说明,覆盖率引导只在支持的架构上提供,团队应把运行环境和时间预算写进自己的测试说明。
失败输入怎样变成可回归的语料
如果 fuzz target 调用了 t.Fatal、其他失败方法,或者解析器发生 panic,工具链会保存导致失败的输入。默认的用户种子与失败语料都位于包内的 testdata/fuzz/ 目录;因此修复代码后,先用普通测试重放它,再重新开始模糊测试。
# 将历史失败语料作为普通测试重放,便于快速确认修复是否生效。 go test -run=FuzzParsePairs # 只重放某一条具体语料时,把命令输出中的语料标识接在名称后面。 go test -run='FuzzParsePairs/语料标识'
失败样例不是“随机噪声”,而是一个可以长期保留的回归用例。修复解析器后不要删除它;如果它暴露的是无效输入处理、边界长度或状态污染问题,保留后能避免未来重构再次引入同类错误。

让 fuzz target 保持快速、确定和无共享状态
解析器模糊测试会高频调用目标函数,因此每轮都应尽量短小。不要依赖当前时间、随机全局变量、网络服务或测试之间共享的可变状态;需要临时数据时,在当前调用内创建并释放。解析器如果本身要访问外部资源,可以先把输入解析层拆出来,对纯函数做 fuzz,再为资源适配层写常规集成测试。
| 现象 | 优先检查 | 处理方式 |
|---|---|---|
| 一开始就失败 | 种子或断言是否与解析契约一致 | 修正种子、缩小断言,不要吞掉真正的 panic |
| 一直没有新覆盖 | 解析器是否过早拒绝输入 | 补充能到达深层分支的结构化种子 |
| 同一输入结果不稳定 | 共享状态、时间和随机数 | 让目标函数只依赖传入参数 |
| 修复后又出现旧问题 | 失败语料是否被保存 | 把语料放入 testdata/fuzz/FuzzParsePairs |
一份可以直接照着改的检查清单
- 函数名是
FuzzXxx,参数只有*testing.F,文件名以_test.go结尾。 f.Add的类型和f.Fuzz的参数顺序一致。- 错误输入走明确的 error 分支,panic 才作为需要修复的失败。
- 成功结果有最小不变量,而不是只判断“没有报错”。
- 先跑
go test ./...,再用-fuzztime给持续实验设置上限。 - 失败语料进入回归路径,修复后仍能用
-run重放。
解析器一定要用 []byte 做 fuzz 参数吗? 不一定。testing.F 还支持字符串、布尔值、整数和浮点数等内置类型;选哪一种,取决于解析器实际接收的边界。二进制协议更适合 []byte,文本协议也可以直接使用 string。
f.Add 能代替 testdata 目录吗? 小而稳定的种子适合写在代码里;较大的失败输入或需要长期保留的回归语料,更适合放进 testdata/fuzz/FuzzParsePairs。两者都会参与默认种子集合,选择标准是可读性和维护成本。
把解析契约、种子、成功不变量和失败语料连起来,Go Fuzz 才会从一次随机实验变成持续可回归的解析器测试。
Go 测试怎样自动清理临时资源
- 上一篇
- Go 测试怎样自动清理临时资源
- 下一篇
- Go bytes.Buffer 怎样复用缓冲区又避免容量过大
-
- Golang · Go教程 | 56分钟前 | go · Go 交叉编译 GOARCH GOOS CGO_ENABLED
- Go 交叉编译怎样关闭 CGO
- 487浏览 收藏
-
- Golang · Go教程 | 1小时前 | go · Go教程 · go.work Go Modules go work 多模块联调
- Go work 怎样管理多个模块联调
- 245浏览 收藏
-
- Golang · Go教程 | 1小时前 | Go教程 · Go 模块依赖 Go Modules go mod why 依赖路径
- Go mod why 怎样解释模块引入路径
- 394浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go net/url 怎样只修改查询参数而保留路径
- 274浏览 收藏
-
- Golang · Go教程 | 1小时前 | goroutine · go ·
- Go trace 怎样观察 goroutine 调度
- 351浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go pprof 怎样定位 CPU 热点函数
- 321浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go benchmark 怎样排除初始化耗时
- 266浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 582次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 620次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 654次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 619次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 435次使用
-
- Golang 实现Redis 协议解析器的解决方案
- 2022-12-23 109浏览
-
- 一文详解Go语言单元测试的原理与使用
- 2022-12-29 377浏览
-
- Golang 单元测试和基准测试实例详解
- 2022-12-23 275浏览
-
- 一文带你了解Go语言中的单元测试
- 2022-12-27 485浏览
-
- Go1.18新特性工作区模糊测试及泛型的使用详解
- 2022-12-30 131浏览

