本文源码:https://github.com/why444216978/go1.18-features-demo
目录
Fuzzing
Fuzzing,又叫 fuzz testing,中文叫做模糊测试或随机测试。其本质上是一种自动化测试技术,更具体一点,它是一种基于随机输入的自动化测试技术,常被用于发现处理用户输入的代码中存在的bug和问题。
在具体实现上,Fuzzing不需要像单元测试那样使用预先定义好的数据集作为程序输入,而是会通过数据构造引擎自行构造或基于开发人员提供的初始数据构造一些随机数据,并作为输入提供给我们的程序,然后监测程序是否出现panic、断言失败、无限循环等。这些构造出来的随机数据被称为语料(corpus)。另外Fuzz testing不是一次性执行的测试,如果不限制执行次数和执行时间,Fuzz testing会一直执行下去,因此它也是一种持续测试的技术。
传统的代码 review、静态分析、人工测试和单元测试等,都无法穷尽所有输入组合,尤其是难于模拟一些非法的、意料之外的输入数据,导致程序运行崩溃。Fuzzing 可以随机生成对应方法的参数,并将这些数据作为输入参数传给对应函数进行测试,从而发现程序难以发现的 BUG。
一个 fuzz test 单测程序运行流程如下:
for {
从语料库中随机选择一个输入
对输入进行变异
执行变异后的输入并收集代码覆盖率
如果该输入给出了新的覆盖率,则将其添加到语料库中
}
我们实现了一个反转字符串的方法,并通过单测运行程序:
package fuzzing
func ReverseNoCheck(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
package fuzzing
import (
"testing"
)
func TestReverseNoCheck(t *testing.T) {
testcases := []struct {
in, want string
}{
{"Hello, world", "dlrow ,olleH"},
{" ", " "},
{"!12345", "54321!"},
}
for _, tc := range testcases {
rev := ReverseNoCheck(tc.in)
if rev != tc.want {
t.Errorf("Reverse: %q, want %q", rev, tc.want)
}
}
}
B000000347425R:fuzzing weihaoyu$ go test -v -cover
=== RUN TestReverseNoCheck
--- PASS: TestReverseNoCheck (0.00s)
PASS
coverage: 100.0% of statements
ok github.com/why444216978/go1.18-features-demo/fuzzing 0.294s
我们发现单测运行成功,没有任何问题,但是程序真的没有 BUG 吗,我们换个思路:反转后字符串再次反转应该和原字符串相等,我们按照这个逻辑,用 Fuzzing 跑一下看看结果:
package fuzzing
import (
"unicode/utf8"
)
func Reverse(s string) (string, error) {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}
package fuzzing
import (
"testing"
"unicode/utf8"
)
func FuzzReverse(f *testing.F) {
// 设置种子语料
testcases := []string{"Hello, world", " ", "!12345"}
for _, tc := range testcases {
f.Add(tc) // Use f.Add to provide a seed corpus
}
// 执行Fuzzing
f.Fuzz(func(t *testing.T, orig string) {
rev, err1 := Reverse(orig)
if err1 != nil {
return
}
doubleRev, err2 := Reverse(rev)
if err2 != nil {
return
}
if orig != doubleRev {
t.Errorf("Before: %s - %q, after:%s - %q", orig, orig, doubleRev, doubleRev)
}
if utf8.ValidString(orig) && !utf8.ValidString(rev) {
t.Errorf("Reverse produced invalid UTF-8 string %q", rev)
}
})
}
B000000347425R:fuzzing weihaoyu$ go test -fuzz=Fuzz -fuzztime 10s
fuzz: elapsed: 0s, gathering baseline coverage: 0/40 completed
failure while testing seed corpus entry: FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a
fuzz: elapsed: 0s, gathering baseline coverage: 2/40 completed
--- FAIL: FuzzReverse (0.03s)
--- FAIL: FuzzReverse (0.00s)
fuzzing_test.go:42: Before: � - "\x83", after:� - "�"
FAIL
exit status 1
FAIL github.com/why444216978/go1.18-features-demo/fuzzing 0.353s
果然,并没有像我们想象的那么顺利,当输入非法字符时,程序并没有按照预期执行,修复方式也很简单,我们对传入的字符串进行 utf8 合法性校验,校验失败返回错误即可:
package fuzzing
import (
"errors"
"unicode/utf8"
)
func Reverse(s string) (string, error) {
if !utf8.ValidString(s) {
return s, errors.New("input is not valid UTF-8")
}
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r), nil
}
再次执行 fuzzing 测试:
B000000347425R:fuzzing weihaoyu$ go test -fuzz=Fuzz -fuzztime 10s
fuzz: elapsed: 0s, gathering baseline coverage: 0/40 completed
fuzz: elapsed: 0s, gathering baseline coverage: 40/40 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 435074 (144992/sec), new interesting: 3 (total: 43)
fuzz: elapsed: 6s, execs: 824724 (129835/sec), new interesting: 3 (total: 43)
fuzz: elapsed: 9s, execs: 1142907 (106093/sec), new interesting: 4 (total: 44)
fuzz: elapsed: 10s, execs: 1220254 (71977/sec), new interesting: 4 (total: 44)
PASS
ok github.com/why444216978/go1.18-features-demo/fuzzing 10.860s
go work
当我们需要在本地项目中调试一个未发布的代码库或版本时,目前通常使用的方式是在 go.mod 文件中使用 replace 语句替换成本地目录,但这种使用方式需要我们时刻记着 go.mod 文件里有一行不能提交的 replace 代码,一旦提到代码库中会对其他使用者造成影响。
从1.18开始,go 命令现在支持“工作区”模式。如果在工作目录或父目录下找到 go.wok 文件,或者使用 GOWORK 环境变量指定了一个工作文件,它会将 go 命令置于工作区模式。在工作区模式下,go.work 文件被优先用于解析包模块,如果在 go.wok 文件中找不到对应包,才会走 go.mod 逻辑。并且 go.work 只是我们本地工作区,可以通过 .gitignore 文件忽略提交。
文字描述起来可能比较抽象,我们看个例子就明白怎么用了,我们定义一个文件,引入一个未发布的包 module,调用里面的 Test 方法:
package module
func Test() string {
return "module2.Tets"
}
package main
import (
"fmt"
"github.com/why444216978/go1.18-features-demo/workspaces/module"
)
func main() {
fmt.Println(module.Test())
}
B000000347425R:workspace weihaoyu$ go run main.go
main.go:6:2: no required module provides package github.com/why444216978/go1.18-features-demo/workspaces/module; to add it:
go get github.com/why444216978/go1.18-features-demo/workspaces/module
B000000347425R:workspace weihaoyu$
由于我们引用的 module 包还未推到远端仓库,所以我们通过以下命令定义 go.work 文件,然后再运行,发现运行成功:
B000000347425R:workspace weihaoyu$ pwd
/Users/weihaoyu/go/go1.18-features-demo/workspace
B000000347425R:workspace weihaoyu$ go work init ./module/
B000000347425R:workspace weihaoyu$ cat go.work
B000000347425R:workspace weihaoyu$ cat go.work
go 1.18
use ./module/
B000000347425R:workspace weihaoyu$ go run main.go
module2.Tets
B000000347425R:workspace weihaoyu$
泛型
泛型比较简单,官方文档介绍的已经很详细了,也不复杂,就不再赘述了,按照源码的 _test.go 文件,写了 4 种用法的 demo,已经放到文首的 Github 上了,读者可以自行学习。
其他
我个人认为,上面两个是目前看是最实用的新功能,除此之外还有一些调整,在这里就不赘述了,直接看官方文档和对应 _test 代码去使用,都比较简单:
- 部分工具命令调整(比如 go get 只专心处理 go.mod 依赖项,安装可执行文件改为 go install)
- 新增底包(比如 debug/buildinfo、net/netip 等)
- 部分底包调整(比如 Mutex 和 RWMutex 支持非阻塞的 TryLock)
- 运行优化(比如优化 gc 部分场景占用更少内存、切片扩容优化)
- 编译优化(部分处理器编译性能提升)
- 其他