导航菜单

基准测试与示例测试

基准测试与示例测试

基准测试(Benchmark)用于测量函数或代码的性能表现,帮助发现性能瓶颈和验证优化效果。示例测试(Example)则为文档提供可运行的代码示例,同时验证示例输出是否正确。两者都是 Go testing 包的重要组成部分。

基准测试(Benchmark)

编写基准测试

基准测试函数以 Benchmark 开头,接收 *testing.B 参数:

// string/string_test.go
package string

import (
    "strings"
    "testing"
)

func BenchmarkConcat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        strings.Join([]string{"Hello", ", ", "World", "!"}, "")
    }
}

func BenchmarkConcatBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        sb.WriteString("Hello")
        sb.WriteString(", ")
        sb.WriteString("World")
        sb.WriteString("!")
        sb.String()
    }
}

运行基准测试

# 运行所有基准测试
$ go test -bench=. ./string/
goos: darwin
goarch: arm64
pkg: myproject/string
BenchmarkConcat-8              12567843                89.32 ns/op            32 B/op          1 allocs/op
BenchmarkConcatBuilder-8       28901234                40.15 ns/op           128 B/op          1 allocs/op
PASS
ok      myproject/string       2.345s

# 运行特定基准测试
$ go test -bench=BenchmarkConcat ./string/

# 指定运行时间
$ go test -bench=. -benchtime=5s ./string/

# 指定最小运行次数
$ go test -bench=. -benchtime=1000x ./string/

# 运行基准测试并显示内存分配信息
$ go test -bench=. -benchmem ./string/

基准测试结果分析

结果字段解读

BenchmarkConcat-8              12567843                89.32 ns/op            32 B/op          1 allocs/op
│                  │                   │                       │                  │
│                  │                   │                       │                  └─ 每次操作内存分配次数
│                  │                   │                       └─ 每次操作内存分配字节数
│                  │                   └─ 每次操作耗时(纳秒)
│                  └─ 总运行次数
└─ 基准测试名称(-8 表示 GOMAXPROCS=8)
字段含义说明
BenchmarkConcat-8测试名称-8 表示使用的 CPU 核心数
12567843运行次数框架自动决定,使总运行时间约 1 秒
89.32 ns/op每次操作耗时纳秒/操作,越小越好
32 B/op每次操作内存分配字节/操作,越小越好
1 allocs/op每次操作分配次数次数/操作,越少越好

对比不同实现

package string

import (
    "fmt"
    "strings"
    "testing"
)

// 实现 1:使用 + 拼接
func concatPlus(parts []string) string {
    var s string
    for _, p := range parts {
        s += p
    }
    return s
}

// 实现 2:使用 strings.Join
func concatJoin(parts []string) string {
    return strings.Join(parts, "")
}

// 实现 3:使用 strings.Builder
func concatBuilder(parts []string) string {
    var sb strings.Builder
    for _, p := range parts {
        sb.WriteString(p)
    }
    return sb.String()
}

// 准备测试数据
var parts = []string{"Go", "is", "a", "wonderful", "programming", "language"}

func BenchmarkPlus(b *testing.B) {
    for i := 0; i < b.N; i++ {
        concatPlus(parts)
    }
}

func BenchmarkJoin(b *testing.B) {
    for i := 0; i < b.N; i++ {
        concatJoin(parts)
    }
}

func BenchmarkBuilder(b *testing.B) {
    for i := 0; i < b.N; i++ {
        concatBuilder(parts)
    }
}
$ go test -bench=. -benchmem
BenchmarkPlus-8        8234567    145.2 ns/op    360 B/op    12 allocs/op
BenchmarkJoin-8       28901234     41.5 ns/op     48 B/op     1 allocs/op
BenchmarkBuilder-8    32145678     37.2 ns/op     64 B/op     1 allocs/op
PASS

ResetTimer 与 StopTimer

ResetTimer / StopTimer / StartTimer

这三个方法用于精确控制基准测试的计时范围。在基准测试中,如果包含了不希望被计入耗时的准备工作(如创建测试数据),应该使用 b.StopTimer() 暂停计时,准备完毕后用 b.StartTimer()b.ResetTimer() 恢复计时。

基本用法

func BenchmarkLargeDataProcessing(b *testing.B) {
    // 暂停计时:创建测试数据不需要计入
    b.StopTimer()
    data := makeLargeTestData()  // 耗时的准备工作
    b.StartTimer()  // 开始计时

    for i := 0; i < b.N; i++ {
        processData(data)
    }
}

b.ResetTimer()

b.ResetTimer() 重置计时器并清零内存分配计数器:

func BenchmarkWithSetup(b *testing.B) {
    // 跳过不需要的初始化时间
    heavySetup()  // 耗时的初始化

    // 重置计时器:忽略上面的初始化时间
    b.ResetTimer()

    for i := 0; i < b.N; i++ {
        targetFunction()
    }
}

使用 b.Run() 进行参数化基准测试

func BenchmarkMapOperations(b *testing.B) {
    sizes := []int{10, 100, 1000, 10000}

    for _, size := range sizes {
        b.Run(fmt.Sprintf("size_%d", size), func(b *testing.B) {
            // 准备测试数据
            b.StopTimer()
            m := make(map[int]int, size)
            for i := 0; i < size; i++ {
                m[i] = i * 2
            }
            b.StartTimer()

            for i := 0; i < b.N; i++ {
                _ = m[size/2]  // 查找操作
            }
        })
    }
}
$ go test -bench=BenchmarkMapOperations -benchmem
BenchmarkMapOperations/size_10-8         215678912     5.523 ns/op       0 B/op    0 allocs/op
BenchmarkMapOperations/size_100-8        126345678     9.456 ns/op       0 B/op    0 allocs/op
BenchmarkMapOperations/size_1000-8        85432167    13.876 ns/op       0 B/op    0 allocs/op
BenchmarkMapOperations/size_10000-8       62145678    19.234 ns/op       0 B/op    0 allocs/op

示例测试(Example)

示例测试(Example Test)

示例测试既是文档也是测试。它展示函数的使用方法,同时验证输出是否正确。godoc 工具会自动提取示例测试生成文档。示例测试函数以 Example 开头。

基本示例测试

// math/math_test.go
package math

import "fmt"

// 展示 Add 函数的基本用法
func ExampleAdd() {
    sum := Add(2, 3)
    fmt.Println(sum)
    // Output: 5
}

// 展示 Divide 函数的用法(包括错误处理)
func ExampleDivide() {
    result, err := Divide(10, 3)
    if err != nil {
        fmt.Println("错误:", err)
        return
    }
    fmt.Printf("%.2f\n", result)
    // Output: 3.33
}

// 无输出的示例(仅作为文档)
func Example() {
    fmt.Println("Hello, Go!")
}

示例测试的命名规则

示例测试的命名决定了它们与哪个函数/类型/方法关联:

package sort

// ExampleInts — 关联包级别函数
func ExampleInts() {
    ints := []int{3, 1, 4, 1, 5}
    Ints(ints)
    fmt.Println(ints)
    // Output: [1 1 3 4 5]
}

// ExamplePerson_String — 关联 Person 类型的 String 方法
func ExamplePerson_String() {
    p := Person{Name: "Alice", Age: 30}
    fmt.Println(p.String())
    // Output: Alice (30)
}

// ExamplePerson_New — 关联 Person 类型(作为构造函数示例)
func ExamplePerson_New() {
    p := NewPerson("Bob", 25)
    fmt.Printf("%s is %d years old\n", p.Name, p.Age)
    // Output: Bob is 25 years old
}

// ExamplePerson_Validate_error — 关联 Validate 方法,展示错误场景
func ExamplePerson_Validate_error() {
    p := Person{Name: "", Age: -1}
    err := p.Validate()
    fmt.Println(err)
    // Output: name is empty and age must be positive
}

Unordered Output

当输出顺序不确定时,使用 Unordered output 注释:

func ExampleMapIteration() {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    for k, v := range m {
        fmt.Printf("%s:%d ", k, v)
    }
    fmt.Println()
    // Unordered output: a:1 b:2 c:3
    // 或 // Unordered output: c:3 a:1 b:2
}

没有 Output 注释的示例

不包含 // Output: 注释的示例测试仅作为文档展示,不会被执行验证

// 仅文档示例,不验证输出
func Example() {
    fmt.Println("这是一个文档示例")
    fmt.Println("不会被测试框架验证")
}

go test 命令详解

常用命令参数

参数说明示例
go test运行当前目录的测试go test
go test ./...运行所有目录的测试go test ./...
-v详细输出模式go test -v
-run <regex>运行匹配正则的测试go test -run TestAdd
-bench <regex>运行基准测试go test -bench=.
-benchtime <dur>基准测试运行时间go test -bench=. -benchtime=5s
-count <n>运行测试 n 次go test -count=3
-cover显示覆盖率go test -cover
-coverprofile <f>输出覆盖率到文件go test -coverprofile=c.out
-race检测数据竞争go test -race
-short跳过耗时测试go test -short
-timeout <dur>设置超时时间go test -timeout 30s
-run + -bench先运行测试再运行基准go test -run=^$ -bench=.
-memprofile <f>输出内存分配分析go test -memprofile=mem.prof
-cpuprofile <f>输出 CPU 分析go test -cpuprofile=cpu.prof

常用命令组合

# 运行所有测试并显示覆盖率
go test -cover ./...

# 运行测试并生成 HTML 覆盖率报告
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# 运行特定测试(正则匹配)
go test -run "TestAdd|TestSub" ./...

# 运行基准测试(不运行单元测试)
go test -run=^$ -bench=. ./...

# 运行基准测试并显示内存信息
go test -bench=. -benchmem ./...

# 使用 race detector 检测数据竞争
go test -race ./...

# 并行运行测试(利用多核)
go test -parallel 4 ./...

# 设置超时时间(默认 10 分钟)
go test -timeout 60s ./...

# 运行指定包的测试
go test ./math/... ./string/...

# 多次运行测试以检测不稳定性(flaky tests)
go test -count=5 ./...

构建标签(Build Tags)

使用构建标签来控制测试条件:

// +build integration

package mytest

func TestDatabase(t *testing.T) {
    // 这个测试只在指定标签时运行
    // go test -tags=integration ./...
}
// 跳过耗时测试
func TestExpensive(t *testing.T) {
    if testing.Short() {
        t.Skip("跳过耗时测试(-short 模式)")
    }
    // 耗时操作...
}

性能分析工具链

# 1. 生成 CPU profile
go test -bench=. -cpuprofile=cpu.prof ./...

# 2. 分析 CPU profile(交互式)
go tool pprof cpu.prof

# 3. 生成火焰图(需要 graphviz)
go tool pprof -http=:8080 cpu.prof
# 浏览器打开 http://localhost:8080

# 4. 内存分配分析
go test -bench=. -memprofile=mem.prof ./...
go tool pprof mem.prof

# 5. 阻塞分析
go test -bench=. -blockprofile=block.prof ./...

测试的最佳实践

1. 测试结构:AAA 模式

AAA 模式(Arrange-Act-Assert)

AAA 是编写测试的经典模式:Arrange(准备)→ Act(执行)→ Assert(断言)。保持这三个部分的清晰分离,使测试更易读易维护。

func TestAdd(t *testing.T) {
    // Arrange:准备测试数据
    a, b := 2, 3
    expected := 5

    // Act:执行被测操作
    result := Add(a, b)

    // Assert:验证结果
    if result != expected {
        t.Errorf("Add(%d, %d) = %d, 期望 %d", a, b, result, expected)
    }
}

2. 测试命名:描述行为而非实现

// ❌ 不好的命名
func TestAdd1(t *testing.T) { ... }
func TestAdd2(t *testing.T) { ... }
func TestAddWorks(t *testing.T) { ... }

// ✅ 好的命名:描述被测行为和期望
func TestAddPositiveNumbers(t *testing.T) { ... }
func TestAddNegativeNumbers(t *testing.T) { ... }
func TestAddReturnsZeroForEmptyInput(t *testing.T) { ... }
func TestAddHandlesOverflow(t *testing.T) { ... }

3. 测试独立性

4. 基准测试的最佳实践

// ✅ 好的基准测试实践
func BenchmarkGood(b *testing.B) {
    // 1. 在循环外准备大型数据
    data := generateTestData()

    // 2. 使用 ResetTimer 排除准备时间
    b.ResetTimer()

    // 3. 循环内只包含要测量的代码
    for i := 0; i < b.N; i++ {
        // 4. 确保结果被使用(防止编译器优化)
        result := process(data)
        if result == nil {
            b.Fatal("意外结果")
        }
    }
}

// ❌ 不好的基准测试
func BenchmarkBad(b *testing.B) {
    for i := 0; i < b.N; i++ {
        data := generateTestData()  // 每次循环都创建数据,扭曲结果
        _ = process(data)
    }
}

5. 使用测试辅助库(简要介绍)

Go 社区常用的测试辅助库:

用途示例
testify断言和 Mockassert.Equal(t, got, want)
httptestHTTP 测试httptest.NewServer()
testing/fstest文件系统测试fstest.MapFS{}
testcontainers-goDocker 集成测试容器化数据库
// 使用 testify 的断言(需要 go get github.com/stretchr/testify)
import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestWithTestify(t *testing.T) {
    assert.Equal(t, 42, compute())           // 失败时继续
    require.NoError(t, doSomething())        // 失败时停止
    assert.True(t, isValid(input))
    assert.Contains(t, result, "expected")
    assert.Greater(t, count, 10)
}

练习题

练习 1:编写基准测试并分析结果

为以下两个 Fibonacci 实现编写基准测试,对比性能差异:

// 递归实现(指数时间复杂度)
func FibRecursive(n int) int {
    if n <= 1 {
        return n
    }
    return FibRecursive(n-1) + FibRecursive(n-2)
}

// 迭代实现(线性时间复杂度)
func FibIterative(n int) int {
    if n <= 1 {
        return n
    }
    a, b := 0, 1
    for i := 2; i <= n; i++ {
        a, b = b, a+b
    }
    return b
}
参考答案

基准测试代码

package fib

import "testing"

func BenchmarkFibRecursive10(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibRecursive(10)
    }
}

func BenchmarkFibRecursive20(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibRecursive(20)
    }
}

func BenchmarkFibRecursive30(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibRecursive(30)
    }
}

func BenchmarkFibIterative10(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibIterative(10)
    }
}

func BenchmarkFibIterative20(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibIterative(20)
    }
}

func BenchmarkFibIterative30(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibIterative(30)
    }
}

// 使用 b.Run 进行参数化基准测试(更优雅)
func BenchmarkFibIterative(b *testing.B) {
    for _, n := range []int{10, 20, 30, 50, 100} {
        b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
            for i := 0; i < b.N; i++ {
                FibIterative(n)
            }
        })
    }
}

典型结果

BenchmarkFibRecursive10-8     52345678    22.1 ns/op       0 B/op    0 allocs/op
BenchmarkFibRecursive20-8       345678   3421.5 ns/op       0 B/op    0 allocs/op
BenchmarkFibRecursive30-8         23456  51234.2 ns/op       0 B/op    0 allocs/op
BenchmarkFibIterative10-8       87654321    13.5 ns/op       0 B/op    0 allocs/op
BenchmarkFibIterative20-8       65432178    18.2 ns/op       0 B/op    0 allocs/op
BenchmarkFibIterative30-8       54321098    21.0 ns/op       0 B/op    0 allocs/op

分析

  1. 递归实现随 n 增大,耗时急剧上升(指数增长),n=30 时已需要约 50μs
  2. 迭代实现始终在约 15-20ns 内完成,几乎不受 n 影响
  3. n=20 时迭代实现比递归快约 188 倍
  4. 两种实现都没有内存分配(0 B/op, 0 allocs/op)

结论:对于 Fibonacci 这类问题,迭代实现远优于递归实现。

练习 2:编写示例测试

为以下 Set 类型编写示例测试,展示创建、添加、删除、查找等操作,并确保输出验证通过:

type Set[T comparable] struct {
    items map[T]bool
}

func NewSet[T comparable]() *Set[T] { ... }
func (s *Set[T]) Add(item T) { ... }
func (s *Set[T]) Remove(item T) { ... }
func (s *Set[T]) Contains(item T) bool { ... }
func (s *Set[T]) Size() int { ... }
func (s *Set[T]) Items() []T { ... }
参考答案

示例测试代码

package set

import "fmt"

func ExampleNewSet() {
    s := NewSet[int]()
    fmt.Println(s.Size())
    // Output: 0
}

func ExampleSet_Add() {
    s := NewSet[string]()
    s.Add("Go")
    s.Add("Python")
    s.Add("Go")  // 重复添加
    fmt.Println(s.Size())
    fmt.Println(s.Contains("Go"))
    // Output: 2
    // true
}

func ExampleSet_Remove() {
    s := NewSet[int]()
    s.Add(1)
    s.Add(2)
    s.Add(3)
    s.Remove(2)
    fmt.Println(s.Size())
    fmt.Println(s.Contains(2))
    // Output: 2
    // false
}

func ExampleSet_Contains() {
    s := NewSet[int]()
    s.Add(42)
    fmt.Println(s.Contains(42))
    fmt.Println(s.Contains(100))
    // Output: true
    // false
}

func ExampleSet_Items() {
    s := NewSet[int]()
    s.Add(3)
    s.Add(1)
    s.Add(2)
    fmt.Println(s.Items())  // 注意:顺序可能不确定
    // Unordered output: [1 2 3]
    // 或:Unordered output: [3 1 2]
}

// 综合使用示例
func Example() {
    s := NewSet[string]()
    s.Add("apple")
    s.Add("banana")
    s.Add("cherry")

    fmt.Println("包含 apple:", s.Contains("apple"))
    fmt.Println("包含 grape:", s.Contains("grape"))
    fmt.Println("大小:", s.Size())

    s.Remove("banana")
    fmt.Println("删除后大小:", s.Size())
    // Output:
    // 包含 apple: true
    // 包含 grape: false
    // 大小: 3
    // 删除后大小: 2
}

运行示例测试

$ go test -v -run=Example
=== RUN   ExampleNewSet
--- PASS: ExampleNewSet (0.00s)
=== RUN   ExampleSet_Add
--- PASS: ExampleSet_Add (0.00s)
=== RUN   ExampleSet_Remove
--- PASS: ExampleSet_Remove (0.00s)
=== RUN   ExampleSet_Contains
--- PASS: ExampleSet_Contains (0.00s)
=== RUN   ExampleSet_Items
--- PASS: ExampleSet_Items (0.00s)
=== RUN   Example
--- PASS: Example (0.00s)
PASS

练习 3:go test 命令练习

假设你有一个项目结构如下:

myproject/
├── calc/
│   ├── calc.go
│   └── calc_test.go
├── utils/
│   ├── utils.go
│   └── utils_test.go
├── integration/
│   └── integration_test.go  // 包含标签:// +build integration
└── go.mod

请写出满足以下需求的 go test 命令:

  1. 运行所有单元测试,显示详细输出
  2. 只运行 calc 包的测试
  3. 运行所有测试并显示覆盖率百分比
  4. 运行集成测试
  5. 只运行基准测试(不运行单元测试)
  6. 运行基准测试,每次至少 3 秒,并显示内存分配信息
参考答案

命令解答

# 1. 运行所有单元测试,显示详细输出
go test -v ./...

# 2. 只运行 calc 包的测试
go test ./calc/
# 或指定特定测试
go test -run=. ./calc/

# 3. 运行所有测试并显示覆盖率百分比
go test -cover ./...

# 4. 运行集成测试(使用构建标签)
go test -tags=integration ./integration/

# 5. 只运行基准测试(不运行单元测试)
go test -run=^$ -bench=. ./...

# 6. 运行基准测试,每次至少 3 秒,显示内存分配
go test -run=^$ -bench=. -benchtime=3s -benchmem ./...

补充常用命令

# 生成 HTML 覆盖率报告
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# 使用 race detector 检测数据竞争
go test -race ./...

# 运行测试 5 次以检测不稳定性
go test -count=5 ./...

# 设置超时时间为 30 秒
go test -timeout=30s ./...

# 并行度设为 4
go test -parallel=4 ./...

# 显示 CPU 和内存 profile
go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof ./...
go tool pprof -http=:8080 cpu.prof

搜索