Go 模糊测试

Go 1.18 内置的模糊测试框架,通过随机输入自动发现代码中的边界情况和潜在崩溃

#type / concept #status / growing #tech / dev #resource / go

[!info] related notes

  • 所属 MOC: Go 测试与质量
  • 前置概念: [[go-testing-basics|Go 测试基础]]
  • 并列概念: [[go-benchmark-testing|Go 基准测试]]

Go 模糊测试

一句话定义

Go 1.18 内置的模糊测试(fuzzing)允许你定义输入种子,由测试引擎自动生成大量随机输入,持续寻找代码中的崩溃、死锁和逻辑错误。

核心机制 / 工作原理

模糊测试函数以 Fuzz 开头,接收 *testing.F 参数:

func FuzzReverse(f *testing.F) {
    // 种子语料:提供有意义的初始输入
    f.Add("hello")
    f.Add("世界")
    f.Add("")

    // 模糊目标:每个输入都会调用此函数
    f.Fuzz(func(t *testing.T, input string) {
        rev := Reverse(input)
        doubleRev := Reverse(rev)
        if input != doubleRev {
            t.Errorf("Reverse(Reverse(%q)) != %q", input, input)
        }
    })
}

执行流程:

  1. go test -fuzz=FuzzReverse 启动模糊测试
  2. 引擎先运行种子语料(seed corpus)作为热身
  3. 引擎对种子进行变异(mutation):随机修改字节、翻转位、插入/删除片段
  4. 每次变异后的输入传入 fuzz target 函数
  5. 如果触发了 panic 或 t.Error/t.Fatal,引擎保存该输入到语料库
  6. 保存的失败用例会在后续 go test 中作为回归测试运行

支持的变异类型:

f.Add 接受的类型决定了变异策略。支持的类型包括:string[]byteintint8/int16/int32/int64uint 系列、float32/float64boolrune。多参数的 fuzz target 可以组合多种类型。

语料库管理:

# 运行模糊测试(持续运行直到发现失败或 Ctrl+C)
go test -fuzz=FuzzReverse -fuzztime=30s

# 仅运行语料库中的回归测试
go test -run=FuzzReverse

# 语料库存放在 testdata/fuzz/FuzzReverse/ 目录下
# 每个文件是一个失败用例,格式为 go-fuzz 的二进制格式

最小例子 / 最小场景

测试一个 URL 解析函数的健壮性:

func FuzzParseURL(f *testing.F) {
    f.Add("https://example.com/path?q=1")
    f.Add("http://localhost:8080")
    f.Add("ftp://files.example.com/pub")
    f.Add("")

    f.Fuzz(func(t *testing.T, rawURL string) {
        u, err := url.Parse(rawURL)
        if err != nil {
            return // 解析失败是可接受的
        }
        // 不变量:解析后重新格式化不应 panic
        _ = u.String()
        // 不变量:scheme 不应包含空格
        if strings.Contains(u.Scheme, " ") {
            t.Errorf("scheme contains space: %q", u.Scheme)
        }
    })
}

为什么重要

  1. 自动化边界发现:人类编写的测试倾向于覆盖”正常路径”,模糊测试擅长找到那些你没想到的边界输入——超长字符串、Unicode 特殊字符、畸形编码等。
  2. 零额外依赖:Go 1.18+ 开箱即用,无需安装 go-fuzz 等第三方工具,降低了使用门槛。
  3. 持续回归:发现的失败用例自动保存为回归测试,防止问题复现。
  4. 安全审计利器:对解析器、编解码器、协议实现等安全敏感代码特别有效,历史上大量 CVE 由模糊测试发现。

边界与易混淆点

  • 模糊测试 vs 基准测试:模糊测试寻找 bug,基准测试衡量性能。两者目标完全不同,不要混淆 Fuzz*Benchmark* 函数。
  • 模糊测试 vs 属性测试:属性测试(如 gopterrapid)是声明式地定义不变量,模糊测试是引擎驱动的变异探索。Go 内置 fuzzing 更接近后者,但可以通过在 fuzz target 中编写断言来模拟属性测试。
  • CI 集成策略:在 CI 中运行完整 fuzzing 不现实(可能需要数小时)。推荐做法:CI 中只运行 go test(回归语料库),定期(如每夜构建)运行 go test -fuzz 发现新用例。
  • 确定性与并行:模糊测试默认使用所有 CPU 核心并行运行,因此错误的复现需要使用相同的随机种子(-fuzzcachedir)。
  • f.Add 的参数数量:fuzz target 的参数数量必须与 f.Add 调用的参数数量一致,否则编译报错。
  • 不支持的类型:自定义结构体不能直接作为 fuzz 参数,需要拆解为基本类型或使用 []byte 后手动反序列化。
创建于 2026/6/25 更新于 2026/6/25