Go 测试与基准测试
AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。
Go 把测试做进了工具链:go test 就是全部,不需要额外的断言库或测试框架。这一篇讲怎么写、怎么跑、怎么定位慢的地方。
环境准备
示例在 Go 1.24 下验证,只用标准库。
第一个测试
// calc/calc.go
package calc
import "errors"
var ErrDivideByZero = errors.New("除以零")
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}
// calc/calc_test.go
package calc
import (
"errors"
"testing"
)
func TestDivide(t *testing.T) {
got, err := Divide(10, 2)
if err != nil {
t.Fatalf("不期望错误:%v", err)
}
if got != 5 {
t.Errorf("Divide(10, 2) = %v,期望 5", got)
}
}
规则:
- 文件名以
_test.go结尾,与被测代码同包(package calc)或外部测试包(package calc_test)。 - 函数名
TestXxx(t *testing.T),Xxx首字母大写。 t.Errorf记录失败并继续;t.Fatalf立即终止当前测试函数。- 测试文件不参与正常构建,也不会进二进制。
表驱动测试
多个用例写成切片,新增用例只加一行:
func TestDivide_Table(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
wantErr error
}{
{name: "正常", a: 10, b: 2, want: 5},
{name: "小数", a: 1, b: 4, want: 0.25},
{name: "负数", a: -8, b: 2, want: -4},
{name: "除以零", a: 1, b: 0, wantErr: ErrDivideByZero},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Divide(tt.a, tt.b)
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Fatalf("错误 = %v,期望 %v", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("不期望错误:%v", err)
}
if got != tt.want {
t.Errorf("结果 = %v,期望 %v", got, tt.want)
}
})
}
}
t.Run 生成子测试,名字会出现在输出里(TestDivide_Table/除以零),可以单独跑:
go test ./... -run 'TestDivide_Table/除以零'
注意:循环里的 tt 在 Go 1.22 起每次迭代都是新变量,可以放心直接用(旧版本才需要 tt := tt)。
辅助函数与清理
func newTestDB(t *testing.T) *sql.DB {
t.Helper() // 失败时报调用方行号
db, err := sql.Open("sqlite", "file::memory:?cache=shared")
if err != nil {
t.Fatalf("打开数据库:%v", err)
}
t.Cleanup(func() { db.Close() }) // 测试结束自动执行,含子测试
return db
}
t.TempDir() 创建测试结束自动删除的目录,适合文件相关逻辑:
func TestWriteFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "out.txt")
if err := os.WriteFile(path, []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil || string(data) != "hi" {
t.Fatalf("读取结果 = %q, %v", data, err)
}
}
并行测试:t.Parallel() 放在测试函数开头,注意共享变量与全局状态。
测试 HTTP handler
func TestHandler(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/hello?name=go", nil)
rec := httptest.NewRecorder()
hello(rec, req)
res := rec.Result()
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
t.Fatalf("状态码 = %d", res.StatusCode)
}
body, _ := io.ReadAll(res.Body)
if !strings.Contains(string(body), "go") {
t.Errorf("响应体 = %s", body)
}
}
httptest 不用真的监听端口,速度快且稳定。
基准测试
func BenchmarkConcat(b *testing.B) {
parts := []string{"a", "b", "c", "d"}
for b.Loop() { // Go 1.24 起的写法
_ = strings.Join(parts, ",")
}
}
旧写法是 for i := 0; i < b.N; i++,仍然可用。运行:
go test -bench=. -benchmem ./...
输出:
BenchmarkConcat-8 12345678 95.2 ns/op 32 B/op 1 allocs/op
三个数字关键:每次操作耗时、每次分配字节数、分配次数。减少 allocs/op 往往比压耗时更有效。
对比两个实现时用 benchstat(golang.org/x/perf/cmd/benchstat):
go test -bench=. -count=10 > old.txt
# 改动代码
go test -bench=. -count=10 > new.txt
benchstat old.txt new.txt
单次跑的差异常常是噪声,多次采样再看统计量。
覆盖率
go test ./... -cover
go test ./... -coverprofile=cover.out
go tool cover -html=cover.out
覆盖率衡量「被执行的行」,不衡量断言质量。80% 不是目标,未覆盖的分支才是要看的:错误处理、边界值、空输入。
Fuzz 测试
func FuzzReverse(f *testing.F) {
f.Add("你好")
f.Add("abc")
f.Fuzz(func(t *testing.T, s string) {
got := Reverse(Reverse(s))
if got != s {
t.Fatalf("双重反转 = %q,原值 %q", got, s)
}
})
}
go test -fuzz=FuzzReverse -fuzztime=30s
Fuzz 会随机生成输入并保存导致失败的用例到 testdata/fuzz/,之后 go test 会自动回归。解析器、序列化、字符串处理特别值得加。
示例测试
func ExampleDivide() {
q, _ := Divide(10, 4)
fmt.Println(q)
// Output: 2.5
}
// Output: 注释让 go test 比对输出,示例同时成为文档。
测试组织习惯
- 测试放在被测包旁边,不建单独的
tests/目录。 - 需要访问未导出标识符就用同包测试;只测公开 API 用
_test包。 - 一个测试只验证一个行为;表驱动的每个用例都要有可读的
name。 - 用
go test ./... -race检查数据竞争,CI 里必跑。 - 慢测试用
testing.Short()分级:
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("跳过集成测试")
}
// ...
}
go test -short ./...
常见错误与注意事项
1. 用 t.Log 当断言
日志不会让测试失败。判断必须落到 t.Error / t.Fatal。
2. t.Fatalf 写在 goroutine 里
Fatalf 只能在测试主 goroutine 调用。子 goroutine 里用 t.Errorf 并通过 channel 汇报。
3. 忘记 defer res.Body.Close()
连接不释放,多个测试后可能耗尽。
4. 测试依赖执行顺序
Go 不保证顺序(开了 -shuffle 更明显)。每个测试自己准备状态。
5. 用真实网络与真实时间
外部服务会让 CI 不稳定。用 httptest 与接口注入;时间用可注入的 now func() time.Time。
6. 基准测试里包含测量外的开销
初始化放在循环之前,b.ResetTimer() 用于排除准备时间。
7. 只跑 go test 不看 -race
并发 bug 在普通测试里常常不显形。CI 加 -race。
小结
testing标准库够用:表驱动 +t.Run是默认写法。t.Helper改行号、t.Cleanup/t.TempDir管资源。- 基准测试看
ns/op、B/op、allocs/op,多次采样再比较。 - 覆盖率找漏测分支,fuzz 找边界崩溃,示例测试兼做文档。
- CI 至少跑
go test ./... -race。