第12章 - 测试、基准测试与性能分析
Go 将测试作为语言生态的一等公民,内置了 testing 包和 go test 命令,无需引入第三方框架即可编写单元测试、基准测试和示例。本章系统讲解 Go 的测试体系与性能分析工具。
12.1 测试文件约定
Go 测试遵循严格的命名约定:
- 测试文件以
_test.go结尾,与被测代码放在同一目录、同一包。 - 测试函数以
Test开头,签名为func TestXxx(t *testing.T)。 - 基准函数以
Benchmark开头,签名为func BenchmarkXxx(b *testing.B)。 - 示例函数以
Example开头。
12.2 编写单元测试
假设有一个待测函数 math.go:
package mathutil
func Add(a, b int) int {
return a + b
}
对应的测试文件 math_test.go:
package mathutil
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; 期望 %d", result, expected)
}
}
12.2.1 t.Error 与 t.Fatal
t.Error/t.Errorf:标记测试失败,但继续执行后续代码。t.Fatal/t.Fatalf:标记测试失败并立即终止当前测试函数。t.Log/t.Logf:记录日志(仅在失败或-v模式下显示)。t.Skip:跳过测试。
12.3 运行测试
go test # 运行当前包的测试
go test ./... # 运行所有包的测试
go test -v # 详细模式,显示每个测试
go test -run TestAdd # 只运行匹配的测试(支持正则)
go test -race # 启用竞态检测
go test -count=1 # 禁用缓存,强制重跑
12.4 表驱动测试
表驱动测试(table-driven test)是 Go 社区推崇的测试模式:将多组输入输出组织成一张表,循环执行,简洁且易扩展:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"正数相加", 2, 3, 5},
{"含零", 0, 5, 5},
{"负数", -1, -1, -2},
{"正负相加", -5, 3, -2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // 子测试
if got := Add(tt.a, tt.b); got != tt.expected {
t.Errorf("Add(%d, %d) = %d; 期望 %d",
tt.a, tt.b, got, tt.expected)
}
})
}
}
t.Run 创建子测试,每个用例独立报告,可单独运行:go test -run TestAdd/负数。
12.5 测试覆盖率
go test -cover # 显示覆盖率百分比
go test -coverprofile=coverage.out # 生成覆盖率数据文件
go tool cover -html=coverage.out # 在浏览器中可视化查看
go tool cover -func=coverage.out # 按函数查看覆盖率
覆盖率帮助识别未被测试触及的代码路径,但需注意:高覆盖率不等于高质量测试,关键在于覆盖重要逻辑和边界条件。
12.6 基准测试
基准测试用于测量代码性能。b.N 由测试框架自动调整,以获得稳定的统计结果:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
func BenchmarkStringConcat(b *testing.B) {
b.ResetTimer() // 重置计时器,排除准备阶段
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 100; j++ {
s += "x"
}
}
}
运行基准测试:
go test -bench=. # 运行所有基准测试
go test -bench=Add # 运行匹配的
go test -bench=. -benchmem # 同时报告内存分配
go test -bench=. -benchtime=5s # 每个基准运行 5 秒
输出示例:
BenchmarkAdd-8 1000000000 0.30 ns/op 0 B/op 0 allocs/op
含义:使用 8 核,执行了 10 亿次,每次操作 0.30 纳秒,每次分配 0 字节内存、0 次分配。
12.7 示例测试
示例函数既是文档又是测试。带有 // Output: 注释的示例会被 go test 验证输出是否一致:
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output: 3
}
示例会自动展示在 pkg.go.dev 的文档页面中,是绝佳的”活文档”。
12.8 测试辅助工具
12.8.1 setup 与 teardown
使用 TestMain 控制测试的全局初始化和清理:
func TestMain(m *testing.M) {
// setup: 准备数据库、临时文件等
setup()
code := m.Run() // 运行所有测试
// teardown: 清理资源
teardown()
os.Exit(code)
}
12.8.2 t.Helper 与 t.Cleanup
func assertEqual(t *testing.T, got, want int) {
t.Helper() // 标记为辅助函数,错误定位到调用处
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestSomething(t *testing.T) {
t.Cleanup(func() {
// 测试结束后自动清理
})
}
12.8.3 第三方断言库 testify
虽然标准库已足够,但社区广泛使用 testify 简化断言:
import "github.com/stretchr/testify/assert"
func TestWithTestify(t *testing.T) {
assert.Equal(t, 5, Add(2, 3))
assert.NoError(t, err)
assert.True(t, condition)
}
12.9 性能分析 pprof
Go 内置强大的性能剖析工具 pprof,可分析 CPU、内存、goroutine、阻塞等。
12.9.1 通过测试生成 profile
go test -cpuprofile=cpu.prof -memprofile=mem.prof -bench=.
go tool pprof cpu.prof
进入交互界面后,常用命令:top(耗时排名)、list 函数名(查看具体行)、web(生成调用图,需 Graphviz)。
12.9.2 在服务中开启 pprof
为长期运行的 HTTP 服务引入 net/http/pprof:
import _ "net/http/pprof" // 空白导入,自动注册路由
func main() {
go http.ListenAndServe("localhost:6060", nil)
// ... 业务逻辑 ...
}
然后访问 http://localhost:6060/debug/pprof/ 或使用:
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 # CPU
go tool pprof http://localhost:6060/debug/pprof/heap # 内存
12.10 执行追踪 trace
go tool trace 提供更细粒度的运行时追踪,可视化 goroutine 调度、GC、系统调用等:
go test -trace=trace.out -bench=.
go tool trace trace.out
12.11 测试最佳实践
- 测试与代码同目录同包,对未导出函数也能测试;如需测试公共 API,可用
包名_test外部测试包。 - 优先表驱动测试,覆盖正常、边界、异常情况。
- 测试应独立、可重复,不依赖执行顺序和外部状态。
- 用
-race运行测试,及早发现并发问题。 - 基准测试关注
allocs/op,减少内存分配往往是优化的关键。 - 先测量后优化:用 pprof 定位真正的性能瓶颈,避免凭直觉过早优化。
12.12 本章小结
本章讲解了 Go 完整的测试体系:单元测试、表驱动测试、子测试、覆盖率统计、基准测试和示例测试,全部由内置的 testing 包和 go test 命令支持。性能分析方面,pprof 和 trace 是定位 CPU、内存瓶颈和并发问题的利器。”测试先行、测量后优化”是 Go 工程文化的重要组成部分。
下一章我们将学习反射与泛型这两项高级特性。