Go语言开发代码自测绝佳go fuzzing用法详解
亲爱的编程学习爱好者,如果你点开了这篇文章,说明你对《Go语言开发代码自测绝佳go fuzzing用法详解》很感兴趣。本篇文章就来给大家详细解析一下,主要介绍一下fuzzing、代码自测,希望所有认真读完的童鞋们,都有实质性的提高。
特别说明
这个真的不是标题党,我写代码20+年,真心认为 go fuzzing 是我见过的最牛逼的代码自测方法。我在用 AC自动机 算法改进关键字过滤效率(提升~50%),改进 mapreduce 对 panic 的处理机制的时候,都通过 go fuzzing 发现了边缘情况的 bug。所以深深的认为,这是我见过最牛逼的代码自测方法,没有之一!
go fuzzing 至今已经发现了代码质量极高的 Go 标准库超过200个bug,见:github.com/dvyukov/go-…
春节程序员之间的祝福经常是,祝你代码永无 bug!虽然调侃,但对我们每个程序员来说,每天都在写 bug,这是事实。代码没 bug 这事,只能证伪,不能证明。即将发布的 Go 1.18 官方提供了一个帮助我们证伪的绝佳工具 - go fuzzing。
Go 1.18 大家最关注的是泛型,然而我真的觉得 go fuzzing 真的是 Go 1.18 最有用的功能,没有之一!
本文我们就来详细看看 go fuzzing:
- 是什么?
- 怎么用?
- 有何最佳实践?
首先,你需要升级到 Go 1.18
Go 1.18 虽然还未正式发布,但你可以下载 RC 版本,而且即使你生产用 Go 更早版本,你也可以开发环境使用 go fuzzing 寻找 bug
go fuzzing 是什么
根据 官方文档 介绍,go fuzzing 是通过持续给一个程序不同的输入来自动化测试,并通过分析代码覆盖率来智能的寻找失败的 case。这种方法可以尽可能的寻找到一些边缘 case,亲测确实发现的都是些平时很难发现的问题。
go fuzzing 怎么用
官方介绍写 fuzz tests 的一些规则:
- 函数必须是 Fuzz开头,唯一的参数是 *testing.F,没有返回值
- Fuzz tests 必须在 *_test.go 的文件里
- 上图中的 fuzz target 是个方法调用 (*testing.F).Fuzz,第一个参数是 *testing.T,然后就是称之为 fuzzing arguments 的参数,没有返回值
- 每个 fuzz test 里只能有一个 fuzz target
- 调用 f.Add(…) 的时候需要参数类型跟 fuzzing arguments 顺序和类型都一致
fuzzing arguments 只支持以下类型:
string, []byte
int, int8, int16, int32/rune, int64
uint, uint8/byte, uint16, uint32, uint64
float32, float64
bool
fuzz target 不要依赖全局状态,会并行跑。
运行 fuzzing tests
如果我写了一个 fuzzing test,比如:
// 具体代码见 https://github.com/zeromicro/go-zero/blob/master/core/mr/mapreduce_fuzz_test.go func FuzzMapReduce(f *testing.F) { ... }
那么我们可以这样执行:
go test -fuzz=MapReduce
我们会得到类似如下结果:
fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers fuzz: elapsed: 3s, execs: 3338 (1112/sec), new interesting: 56 (total: 57) fuzz: elapsed: 6s, execs: 6770 (1144/sec), new interesting: 62 (total: 63) fuzz: elapsed: 9s, execs: 10157 (1129/sec), new interesting: 69 (total: 70) fuzz: elapsed: 12s, execs: 13586 (1143/sec), new interesting: 72 (total: 73) ^Cfuzz: elapsed: 13s, execs: 14031 (1084/sec), new interesting: 72 (total: 73) PASS ok github.com/zeromicro/go-zero/core/mr 13.169s
其中的 ^C 是我按了 ctrl-C 终止了测试,详细解释参考官方文档。
go-zero 的最佳实践
按照我使用下来的经验总结,我把最佳实践初步总结为以下四步:
- 定义 fuzzing arguments,首先要想明白怎么定义 fuzzing arguments,并通过给定的 fuzzing arguments 写 fuzzing target
- 思考 fuzzing target 怎么写,这里的重点是怎么验证结果的正确性,因为 fuzzing arguments 是“随机”给的,所以要有个通用的结果验证方法
- 思考遇到失败的 case 如何打印结果,便于生成新的 unit test
- 根据失败的 fuzzing test 打印结果编写新的 unit test,这个新的 unit test会被用来调试解决fuzzing test发现的问题,并固化下来留给CI 用
接下来我们以一个最简单的数组求和函数来展示一下上述步骤,go-zero 的实际案例略显复杂,文末我会给出 go-zero 内部落地案例,供大家参考复杂场景写法。
这是一个注入了 bug 的求和的代码实现:
func Sum(vals []int64) int64 { var total int64 for _, val := range vals { if val%1e5 != 0 { total += val } } return total }
1. 定义 fuzzing arguments
你至少需要给出一个 fuzzing argument,不然 go fuzzing 没法生成测试代码,所以即使我们没有很好的输入,我们也需要定义一个对结果产生影响的 fuzzing argument,这里我们就用 slice 元素个数作为 fuzzing arguments,然后 Go fuzzing 会根据跑出来的 code coverage 自动生成不同的参数来模拟测试。
func FuzzSum(f *testing.F) { f.Add(10) f.Fuzz(func(t *testing.T, n int) { n %= 20 ... }) }
这里的 n 就是让 go fuzzing 来模拟 slice 元素个数,为了保证元素个数不会太多,我们限制在20以内(0个也没问题),并且我们添加了一个值为10的语料(go fuzzing 里面称之为 corpus),这个值就是让 go fuzzing 冷启动的一个值,具体为多少不重要。
2. 怎么写 fuzzing target
这一步的重点是如何编写可验证的 fuzzing target,根据给定的 fuzzing arguments 写出测试代码的同时,还需要生成验证结果正确性用的数据。
对我们这个 Sum 函数来说,其实还是比较简单的,就是随机生成 n 个元素的 slice,然后求和算出期望的结果。如下:
func FuzzSum(f *testing.F) { rand.Seed(time.Now().UnixNano()) f.Add(10) f.Fuzz(func(t *testing.T, n int) { n %= 20 var vals []int64 var expect int64 for i := 0; i这段代码还是很容易理解的,自己求和和 Sum 求和做比较而已,就不详细解释了。但复杂场景你就需要仔细想想怎么写验证代码了,不过这不会太难,太难的话,可能是对测试函数没有足够理解或者简化。
此时就可以用如下命令跑 fuzzing tests 了,结果类似如下:
$ go test -fuzz=Sum fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers fuzz: elapsed: 0s, execs: 6672 (33646/sec), new interesting: 7 (total: 6) --- FAIL: FuzzSum (0.21s) --- FAIL: FuzzSum (0.00s) sum_fuzz_test.go:34: Error Trace: sum_fuzz_test.go:34 value.go:556 value.go:339 fuzz.go:334 Error: Not equal: expected: 8736932 actual : 8636932 Test: FuzzSum Failing input written to testdata/fuzz/FuzzSum/739002313aceff0ff5ef993030bbde9115541cabee2554e6c9f3faaf581f2004 To re-run: go test -run=FuzzSum/739002313aceff0ff5ef993030bbde9115541cabee2554e6c9f3faaf581f2004 FAIL exit status 1 FAIL github.com/kevwan/fuzzing 0.614s那么问题来了!我们看到了结果不对,但是我们很难去分析为啥不对,你仔细品品,上面这段输出,你怎么分析?
3. 失败 case 如何打印输入
对于上面失败的测试,我们如果能打印出输入,然后形成一个简单的测试用例,那我们就可以直接调试了。打印出来的输入最好能够直接 copy/paste 到新的测试用例里,如果格式不对,对于那么多行的输入,你需要一行一行调格式就太累了,而且这未必就只有一个失败的 case。
所以我们把代码改成了下面这样:
func FuzzSum(f *testing.F) { rand.Seed(time.Now().UnixNano()) f.Add(10) f.Fuzz(func(t *testing.T, n int) { n %= 20 var vals []int64 var expect int64 var buf strings.Builder buf.WriteString("\n") for i := 0; i再跑命令,得到如下结果:
$ go test -fuzz=Sum fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers fuzz: elapsed: 0s, execs: 1402 (10028/sec), new interesting: 10 (total: 8) --- FAIL: FuzzSum (0.16s) --- FAIL: FuzzSum (0.00s) sum_fuzz_test.go:34: Error Trace: sum_fuzz_test.go:34 value.go:556 value.go:339 fuzz.go:334 Error: Not equal: expected: 5823336 actual : 5623336 Test: FuzzSum Messages: 799023, 110387, 811082, 115543, 859422, 997646, 200000, 399008, 7905, 931332, 591988, Failing input written to testdata/fuzz/FuzzSum/26d024acf85aae88f3291bf7e1c6f473eab8b051f2adb1bf05d4491bc49f5767 To re-run: go test -run=FuzzSum/26d024acf85aae88f3291bf7e1c6f473eab8b051f2adb1bf05d4491bc49f5767 FAIL exit status 1 FAIL github.com/kevwan/fuzzing 0.602s4. 编写新的测试用例
根据上面的失败 case 的输出,我们可以 copy/paste 生成如下代码,当然框架是自己写的,输入参数可以直接拷贝进去。
func TestSumFuzzCase1(t *testing.T) { vals := []int64{ 799023, 110387, 811082, 115543, 859422, 997646, 200000, 399008, 7905, 931332, 591988, } assert.Equal(t, int64(5823336), Sum(vals)) }这样我们就可以很方便的调试了,并且能够增加一个有效 unit test,确保这个 bug 再也不会出现了。
go fuzzing 更多经验
Go 版本问题
我相信,Go 1.18 发布了,大多数项目线上代码不会立马升级到 1.18 的,那么 go fuzzing 引入的 testing.F 不能使用怎么办?
线上(go.mod)不升级到 Go 1.18,但是我们本机是完全推荐升级的,那么这时我们只需要把上面的 FuzzSum 放到一个文件名类似 sum_fuzz_test.go 的文件里,然后在文件头加上如下指令即可:
// go:build go1.18 // +build go1.18注意:第三行必须是一个空行,否则就会变成 package 的注释了。
这样我们在线上不管用哪个版本就不会报错了,而我们跑 fuzz testing 一般都是本机跑的,不受影响。
go fuzzing 不能复现的失败
上面讲的步骤是针对简单情况的,但有时根据失败 case 得到的输入形成新的 unit test 并不能复现问题时(特别是有 goroutine 死锁问题),问题就变得复杂起来了,如下输出你感受一下:
go test -fuzz=MapReduce fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers fuzz: elapsed: 3s, execs: 3681 (1227/sec), new interesting: 54 (total: 55) ... fuzz: elapsed: 1m21s, execs: 92705 (1101/sec), new interesting: 85 (total: 86) --- FAIL: FuzzMapReduce (80.96s) fuzzing process hung or terminated unexpectedly: exit status 2 Failing input written to testdata/fuzz/FuzzMapReduce/ee6a61e8c968adad2e629fba11984532cac5d177c4899d3e0b7c2949a0a3d840 To re-run: go test -run=FuzzMapReduce/ee6a61e8c968adad2e629fba11984532cac5d177c4899d3e0b7c2949a0a3d840 FAIL exit status 1 FAIL github.com/zeromicro/go-zero/core/mr 81.471s这种情况下,只是告诉我们 fuzzing process 卡住了或者不正常结束了,状态码是2。这种情况下,一般 re-run 是不会复现的。为什么只是简单的返回错误码2呢?我仔细去看了 go fuzzing 的源码,每个 fuzzing test 都是一个单独的进程跑的,然后 go fuzzing 把模糊测试的进程输出扔掉了,只是显示了状态码。那么我们如何解决这个问题呢?
我仔细分析了之后,决定自己来写一个类似 fuzzing test 的常规单元测试代码,这样就可以保证失败是在同一个进程内,并且会把错误信息打印到标准输出,代码大致如下:
func TestSumFuzzRandom(t *testing.T) { const times = 100000 rand.Seed(time.Now().UnixNano()) for i := 0; i这样我们就可以自己来简单模拟一下 go fuzzing,但是任何错误我们可以得到清晰的输出。这里或许我没研究透 go fuzzing,或者还有其它方法可以控制,如果你知道,感谢告诉我一声。
但这种需要跑很长时间的模拟 case,我们不会希望它在 CI 时每次都被执行,所以我把它放在一个单独的文件里,文件名类似 sum_fuzzcase_test.go,并在文件头加上了如下指令:
// go:build fuzz // +build fuzz这样我们需要跑这个模拟 case 的时候加上 -tags fuzz 即可,比如:
go test -tags fuzz ./...复杂用法示例
上面介绍的是一个示例,还是比较简单的,如果遇到复杂场景不知道怎么写,可以先看看 go-zero 是如何落地 go fuzzing 的,如下所示:
MapReduce - github.com/zeromicro/g…
模糊测试了 死锁 和 goroutine leak,特别是 chan + goroutine 的复杂场景可以借鉴
stringx - github.com/zeromicro/g…
模糊测试了常规的算法实现,对于算法类场景可以借鉴
以上就是《Go语言开发代码自测绝佳go fuzzing用法详解》的详细内容,更多关于golang的资料请关注golang学习网公众号!

- 上一篇
- Go单元测试对GORM进行Mock测试

- 下一篇
- go zero微服务实战系服务拆分
-
- Golang · Go教程 | 6小时前 |
- DebianOpenSSL安装失败的终极解决方案
- 501浏览 收藏
-
- Golang · Go教程 | 7小时前 |
- Debian数据快速提取技巧
- 216浏览 收藏
-
- Golang · Go教程 | 10小时前 |
- Debian系统JS依赖管理终极攻略
- 218浏览 收藏
-
- Golang · Go教程 | 11小时前 |
- Debian上Hadoop作业调度实用技巧
- 100浏览 收藏
-
- Golang · Go教程 | 12小时前 |
- Go语言闭包误区与匿名函数深度解析
- 222浏览 收藏
-
- Golang · Go教程 | 12小时前 |
- Debian系统安全回收数据的正确攻略
- 111浏览 收藏
-
- Golang · Go教程 | 14小时前 |
- Debian高效fetch技巧与使用攻略
- 125浏览 收藏
-
- Golang · Go教程 | 20小时前 |
- Debian邮件服务器升级维护攻略
- 474浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 508次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 484次学习
-
- 笔灵AI生成答辩PPT
- 探索笔灵AI生成答辩PPT的强大功能,快速制作高质量答辩PPT。精准内容提取、多样模板匹配、数据可视化、配套自述稿生成,让您的学术和职场展示更加专业与高效。
- 15次使用
-
- 知网AIGC检测服务系统
- 知网AIGC检测服务系统,专注于检测学术文本中的疑似AI生成内容。依托知网海量高质量文献资源,结合先进的“知识增强AIGC检测技术”,系统能够从语言模式和语义逻辑两方面精准识别AI生成内容,适用于学术研究、教育和企业领域,确保文本的真实性和原创性。
- 24次使用
-
- AIGC检测-Aibiye
- AIbiye官网推出的AIGC检测服务,专注于检测ChatGPT、Gemini、Claude等AIGC工具生成的文本,帮助用户确保论文的原创性和学术规范。支持txt和doc(x)格式,检测范围为论文正文,提供高准确性和便捷的用户体验。
- 30次使用
-
- 易笔AI论文
- 易笔AI论文平台提供自动写作、格式校对、查重检测等功能,支持多种学术领域的论文生成。价格优惠,界面友好,操作简便,适用于学术研究者、学生及论文辅导机构。
- 40次使用
-
- 笔启AI论文写作平台
- 笔启AI论文写作平台提供多类型论文生成服务,支持多语言写作,满足学术研究者、学生和职场人士的需求。平台采用AI 4.0版本,确保论文质量和原创性,并提供查重保障和隐私保护。
- 35次使用
-
- Golangmap实践及实现原理解析
- 2022-12-28 505浏览
-
- 试了下Golang实现try catch的方法
- 2022-12-27 502浏览
-
- Go语言中Slice常见陷阱与避免方法详解
- 2023-02-25 501浏览
-
- Golang中for循环遍历避坑指南
- 2023-05-12 501浏览
-
- Go语言中的RPC框架原理与应用
- 2023-06-01 501浏览