您现在的位置是:网站首页> 编程资料编程资料
Go1.18新特性工作区模糊测试及泛型的使用详解_Golang_
2023-05-26
455人已围观
简介 Go1.18新特性工作区模糊测试及泛型的使用详解_Golang_
前言
2022年3月15日,Google发布了万众瞩目的Golang 1.18,带来了好几个重大的新特性,包括:
- 解决本地同时开发多个仓库带来的一些问题的工作区(Workspace)
- 能够自动探测代码分支,随机生成输入,并且检查代码是否会panic的模糊测试(Fuzzing Test)
- 众多开发者盼星星盼月亮终于等到的泛型支持。
本文将简单讲述这三个特性的相关内容。
Go工作区模式(Go Workspace Mode)
现实的情况
多仓库同时开发
在实际的开发工作中,我们经常会同时修改存在依赖关系的多个module,例如在某个service模块上实现需求的同时,也需要对项目组的某个common模块做出修改,整个的工作流就会变成下面这样:

可以看到,每次修改Common库,都需要将代码push到远端,然后再修改本地service仓库的依赖,再通过go mod tidy从远端拉取Common代码,不可谓不麻烦。
有些同学可能会问了,这种情况,在service仓库的go.mod中添加一条replace不就能够解决吗?
但是,如果在go.mod中使用replace,在维护上需要付出额外的心智成本,万一将带有replace的go.mod推到远端代码库了,其他同学不就一脸懵逼了?
多个新仓库开始开发
假设此时我正在开发两个新的模块,分别是:
code.byted.org/SomeNewProject/Common code.byted.org/SomeNewProject/MyService
并且MyService依赖于Common。
在开发过程中,出于各种原因,有可能不会立即将代码推送到远端,那么此时假设我需要本地编译MyService,就会出现go build(或者go mod tidy)自动下载依赖失败,因为此时Common库根本就没有发布到代码库中。
出于和上述“多仓库同时开发”相同的理由,replace也不应该被添加到MyService的go.mod文件中。
工作区模式是什么
Go工作区模式最早出现于Go开发者Michael Matloob在2021年4月提出的一个名为“Multi-Module Workspaces in cmd/go”的提案。
这个提案中提出,新增一个go.work文件,并且在这个文件中指定一系列的本地路径,这些本地路径下的go module共同构成一个工作区(workspace),go命令可以操作这些路径下的go module,在编译时也会优先使用这些go module。
使用如下命令就可以初始化一个工作区,并且生成一个空的go.work文件:
go work init .
新生成的go.work文件内容如下:
go 1.18 directory ./.
go.work文件中,directory指示了工作区的各个module目录,在编译代码时,会优先使用同一个workspace下的module。
在go.work中,也支持使用replace来指定使用本地代码库,但在大多数情况下,更好的做法是将依赖的本地代码库的路径加入directory中。
推荐的使用方法
因为go.work描述的是本地的工作区,所以也是不能提交到远端代码库的,虽然可以在.gitignore中加入这个文件,但是最推荐的做法还是在本地代码库的上层目录使用go.work。
例如上述的“多个新仓库开始开发”的例子,假设我的两个仓库的本地路径分别是:
/Users/bytedance/dev/my_new_project/common /Users/bytedance/dev/my_new_project/my_service
那么我就可以在“/Users/bytedance/dev/my_new_project”目录下生成一个如下内容的go.work:
/Users/bytedance/dev/my_new_project/go.work: go 1.18 directory ( ./common ./my_service )
在上层目录放置go.work,也可以将多个目录组织成一个workspace,并且由于上层目录本身不受git管理,所以也不用去管gitignore之类的问题,是比较省心的方式。
使用时的注意点
目前(go 1.18)仅go build会对go.work做出判断,而go mod tidy并不care Go工作区。
Go模糊测试(Go Fuzzing Test)
为什么Golang要支持模糊测试
从1.18起,模糊测试(Fuzzing Test)作为语言安全的一环,加入了Golang的testing标准库。Golang加入模糊测试的原因非常明显:安全是程序员在构建软件的过程中必不可少且日益重要的考量因素。
Golang至今为止,已经在保障语言安全方面提供了很多的特性和工具,例如强制使用显式类型转换、禁止隐式类型转换、对数组与切片的越界访问检查、通过go.sum对依赖包进行哈希校验等等。
在进入云原生时代之后,Golang成为了云原生基础设施与服务的头部语言之一。这些系统对安全性的要求自然不言而喻。尤其是针对用户的输入,不被用户的输入弄出处理异常、崩溃、被操控是对这些系统的基本要求之一。
这就要求我们的系统在处理任何用户输入的时候都能保持稳定,但是传统的质量保障手段,例如Code Review、静态分析、人工测试、Unit Test等等,在面对日益复杂的系统时,自然就无法穷尽所有可能的输入组合,尤其是一些非常不明显的corner case。
而模糊测试就是业界在解决这方面问题的优秀实践之一,Golang选择支持它也就不难理解了。
模糊测试是什么
模糊测试是一种通过数据构造引擎,辅以开发者可以提供的一些初始数据,自动构造出一些随机数据,作为对程序的输入来进行测试的一种方式。模糊测试可以帮助开发人员发现难以发现的稳定性、逻辑性甚至是安全性方面的错误,特别是当被测系统变得更加复杂时。
模糊测试在具体的实现上,通常可以不依赖于开发测试人员定义好的数据集,取而代之的则是一组通过数据构造引擎自行构造的一系列随机数据。模糊测试会将这些数据作为输入提供给待测程序,并且监测程序是否出现panic、断言失败、无限循环,或者其他什么异常情况。这些通过数据构造引擎生成的数据被称为语料(corpus) 。另外模糊测试其实也是一种持续测试的手段,因为如果不限制执行的次数或者执行的最大时间,它就会一直不停的执行下去。
Golang的模糊测试由于被实现在了编译器工具链中,所以采用了一种名为“覆盖率引导的fuzzing”的入参生成技术,大致运行过程如下:

Golang的模糊测试如何使用
Golang的模糊测试在使用时,可以简单地直接使用,也可以自己提供一些初始的语料。
最简单的实践例子
模糊测试的函数也是放在xxx_test.go里的,编写一个最简单的模糊测试例子(明显的除0错误):
package main import "testing" import "fmt" func FuzzDiv(f *testing.F) { f.Fuzz(func(t *testing.T, a, b int) { fmt.Println(a/b) }) } 可以看到类似于单元测试,模糊测试的函数名都是FuzzXxx格式,且接受一个testing.F指针对象。
然后在函数中使用f.Fuzz对指定的函数进行模糊测试,被测试的函数的第一个参数必须是“*testing.T”类型,后面可以跟任意多个基本类型的参数。
编写完成之后,使用这样的命令来启动模糊测试:
go test -fuzz .
模糊测试默认会一直进行下去,只要被测试的函数不panic不出错。可以通过“-fuzztime”选项来限制模糊测试的时间:
go test -fuzztime 10s -fuzz .
使用模糊测试对上述代码进行测试时,会碰到产生panic的情况,此时模糊测试会输出如下信息:
warning: starting with empty corpus
fuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)
fuzz: elapsed: 0s, execs: 1 (65/sec), new interesting: 0 (total: 0)
--- FAIL: FuzzDiv (0.02s)
--- FAIL: FuzzDiv (0.00s)
testing.go:1349: panic: runtime error: integer divide by zero
goroutine 11 [running]:
runtime/debug.Stack()
/Users/bytedance/.mytools/go/src/runtime/debug/stack.go:24 +0x90
testing.tRunner.func1()
/Users/bytedance/.mytools/go/src/testing/testing.go:1349 +0x1f2
panic({0x1196b80, 0x12e3140})
/Users/bytedance/.mytools/go/src/runtime/panic.go:838 +0x207
mydev/fuzz.FuzzDiv.func1(0x0?, 0x0?, 0x0?)
/Users/bytedance/Documents/dev_test/fuzz/main_test.go:8 +0x8c
reflect.Value.call({0x11932a0?, 0x11cbf68?, 0x13?}, {0x11be123, 0x4}, {0xc000010420, 0x3, 0x4?})
/Users/bytedance/.mytools/go/src/reflect/value.go:556 +0x845
reflect.Value.Call({0x11932a0?, 0x11cbf68?, 0x514?}, {0xc000010420, 0x3, 0x4})
/Users/bytedance/.mytools/go/src/reflect/value.go:339 +0xbf
testing.(*F).Fuzz.func1.1(0x0?)
/Users/bytedance/.mytools/go/src/testing/fuzz.go:337 +0x231
testing.tRunner(0xc000003a00, 0xc00007e3f0)
/Users/bytedance/.mytools/go/src/testing/testing.go:1439 +0x102
created by testing.(*F).Fuzz.func1
/Users/bytedance/.mytools/go/src/testing/fuzz.go:324 +0x5b8
Failing input written to testdata/fuzz/FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
To re-run:
go test -run=FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
FAIL
exit status 1
FAIL mydev/fuzz 0.059s
其中的:
Failing input written to testdata/fuzz/FuzzDiv/2058e4e611665fa289e5c0098bad841a6785bf79d30e47b96d8abcb0745a061c
这一行表示模糊测试将出现panic的测试入参保存到了这个文件里面,此时尝试输出这个文件的内容:
go test fuzz v1 int(-60) int(0)
就可以看到引发panic的入参,此时我们就可以根据入参检查我们的代码是哪里有问题。当然,这个简单的例子就是故意写了个除0错误。
提供自定义语料
Golang的模糊测试还允许开发者自行提供初始语料,初始语料可以通过“f.Add”方法提供,也可以将语料以上面的“Failing input”相同的格式,写入“testdata/fuzz/FuzzXXX/自定义语料文件名”中。
使用时的注意点
目前Golang的模糊测试仅支持被测试的函数使用这些类型的参数:
[]byte, string, bool, byte, rune, float32, float64,
int, int8, int16
