第 13 章:测试与性能优化

jerry北京市2026年5月9日Go 23 次阅读 约 14 分钟
第 13 章:测试与性能优化

掌握 Go 的测试框架、表驱动测试、基准测试、pprof 性能分析和常见优化技巧。


13.1 单元测试

Go 内置测试框架,测试文件以 _test.go 结尾:

// calc.go
package calc

func Add(a, b int) int {
    return a + b
}

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("除数不能为零")
    }
    return a / b, nil
}
// calc_test.go
package calc

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d, 期望 5", result)
    }
}

运行测试:

go test ./...              # 运行所有测试
go test -v ./...           # 详细输出
go test -run TestAdd ./... # 运行匹配的测试
go test -count=1 ./...     # 禁用缓存
go test -cover ./...       # 显示覆盖率

13.2 表驱动测试(推荐模式)

func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    float64
        want    float64
        wantErr bool
    }{
        {"正常除法", 10, 2, 5, false},
        {"除以零", 10, 0, 0, true},
        {"负数除法", -10, 2, -5, false},
        {"小数除法", 1, 3, 0.3333333333333333, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if got != tt.want {
                t.Errorf("Divide() = %v, want %v", got, tt.want)
            }
        })
    }
}

13.3 测试辅助函数

// t.Helper() 标记辅助函数,错误报告时显示调用者的行号
func assertEqual(t *testing.T, got, want int) {
    t.Helper()
    if got != want {
        t.Errorf("got %d, want %d", got, want)
    }
}

// t.Cleanup() 注册清理函数
func TestWithCleanup(t *testing.T) {
    tmpFile := createTempFile(t)
    t.Cleanup(func() {
        os.Remove(tmpFile)
    })
    // 测试逻辑...
}

// t.Parallel() 并行执行测试
func TestParallel(t *testing.T) {
    t.Parallel()
    // 这个测试会和其他 Parallel 测试并行执行
}

13.4 基准测试

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}

// 带初始化的基准测试
func BenchmarkDivide(b *testing.B) {
    b.ResetTimer()  // 重置计时器,排除初始化时间
    for i := 0; i < b.N; i++ {
        Divide(10, 3)
    }
}

// 内存分配统计
func BenchmarkSliceAppend(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        s := make([]int, 0)
        for j := 0; j < 1000; j++ {
            s = append(s, j)
        }
    }
}

运行基准测试:

go test -bench=. -benchmem ./...
# 输出示例:
# BenchmarkAdd-8       1000000000    0.25 ns/op    0 B/op    0 allocs/op
# BenchmarkSliceAppend-8   50000    30000 ns/op    40960 B/op    11 allocs/op

对比优化效果

// 优化前:不预分配
func BenchmarkNoPrealloc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := make([]int, 0)
        for j := 0; j < 1000; j++ {
            s = append(s, j)
        }
    }
}

// 优化后:预分配容量
func BenchmarkPrealloc(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := make([]int, 0, 1000)
        for j := 0; j < 1000; j++ {
            s = append(s, j)
        }
    }
}

13.5 pprof 性能分析

CPU 分析

import "runtime/pprof"

f, _ := os.Create("cpu.prof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()

// 你的代码...

内存分析

f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()

HTTP pprof(线上服务推荐)

import _ "net/http/pprof"

go func() {
    http.ListenAndServe(":6060", nil)
}()

访问 http://localhost:6060/debug/pprof/ 查看各项指标。

分析 profile

# 交互式分析
go tool pprof cpu.prof

# 常用命令
# top       查看耗时最多的函数
# list func 查看函数的逐行耗时
# web       生成调用图(需要 graphviz)

# 直接生成火焰图
go tool pprof -http=:8080 cpu.prof

13.6 常见性能优化技巧

1. 预分配切片容量

// 差
s := make([]int, 0)
for i := 0; i < n; i++ {
    s = append(s, i)
}

// 好
s := make([]int, 0, n)
for i := 0; i < n; i++ {
    s = append(s, i)
}

2. 字符串拼接用 strings.Builder

// 差:每次拼接都分配新内存
s := ""
for i := 0; i < 1000; i++ {
    s += "hello"
}

// 好:使用 Builder
var b strings.Builder
for i := 0; i < 1000; i++ {
    b.WriteString("hello")
}
s := b.String()

3. 使用 sync.Pool 复用对象

var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process() {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufPool.Put(buf)
    }()
    // 使用 buf...
}

4. 避免不必要的内存逃逸

// 逃逸到堆
func createUser() *User {
    u := User{Name: "Alice"}
    return &u  // u 逃逸到堆
}

// 不逃逸(如果调用者不需要指针)
func createUser() User {
    return User{Name: "Alice"}  // 栈上分配
}

5. 减少接口转换

// 差:频繁的 interface{} 转换
func process(v interface{}) {
    s := v.(string)  // 类型断言有开销
}

// 好:使用具体类型或泛型
func process(s string) {
    // 直接使用
}

13.7 面试要点

  1. Go 测试文件的命名规则?

    • 文件名以 _test.go 结尾
    • 测试函数以 Test 开头,参数为 *testing.T
    • 基准测试以 Benchmark 开头,参数为 *testing.B
  2. 表驱动测试的优势?

    • 易于添加新用例
    • 代码结构清晰
    • 每个用例有名字,失败时容易定位
  3. 如何做性能分析?

    • go test -bench 做基准测试
    • pprof 做 CPU/内存分析
    • -race 检测数据竞争
  4. 常见的性能优化手段?

    • 预分配切片容量
    • strings.Builder 拼接字符串
    • sync.Pool 复用对象
    • 减少内存逃逸
    • 减少接口转换

练习

  1. 为一个排序函数编写表驱动测试
  2. 编写基准测试,对比字符串拼接的不同方式
  3. 使用 pprof 分析一段代码的 CPU 和内存使用
  4. 优化一段有性能问题的代码,用基准测试验证效果

← 上一章:反射与泛型 | 下一章:Web 开发实战 →

评论

登录 后发表评论

暂无评论