G

[Golang] 一次errors的benchmark记录

RoLingG Golang 2026-03-28

一次errors的benchmark记录

本文代码由我和AI携手编写,如有问题全是我菜逼导致的(
package errorsbench

import (
    "errors"
    "fmt"
    "os"
    "testing"
)

// makeErrorChain 构造指定深度的错误链
// depth=1: 只有底层错误
// depth=3: 底层错误 + 2层包装
// depth=5: 底层错误 + 4层包装
func makeErrorChain(depth int) error {
    base := &os.PathError{
        Op:   "open",
        Path: "/tmp/test.txt",
        Err:  os.ErrNotExist,
    }

    err := error(base)
    for i := 0; i < depth-1; i++ {
        err = fmt.Errorf("layer %d: %w", i, err)
    }
    return err
}

// benchmarkOldAs 测试标准库 errors.As(反射版)
func benchmarkOldAs(b *testing.B, chainDepth int) {
    err := makeErrorChain(chainDepth)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var target *os.PathError
        if errors.As(err, &target) {
            _ = target.Path // 防止编译器优化掉
        }
    }
}

// benchmarkNewAsType 测试 errors.AsType(泛型版)
func benchmarkNewAsType(b *testing.B, chainDepth int) {
    err := makeErrorChain(chainDepth)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        if target, ok := errors.AsType[*os.PathError](err); ok {
            _ = target.Path // 防止编译器优化掉
        }
    }
}

// 不同深度的对比测试
func BenchmarkAs_Depth1(b *testing.B)     { benchmarkOldAs(b, 1) }
func BenchmarkAsType_Depth1(b *testing.B) { benchmarkNewAsType(b, 1) }

func BenchmarkAs_Depth3(b *testing.B)     { benchmarkOldAs(b, 3) }
func BenchmarkAsType_Depth3(b *testing.B) { benchmarkNewAsType(b, 3) }

func BenchmarkAs_Depth5(b *testing.B)     { benchmarkOldAs(b, 5) }
func BenchmarkAsType_Depth5(b *testing.B) { benchmarkNewAsType(b, 5) }

// 测试内联效果:小函数包装
func checkWithAs(err error) bool {
    var target *os.PathError
    return errors.As(err, &target)
}

func checkWithAsType(err error) bool {
    _, ok := errors.AsType[*os.PathError](err)
    return ok
}

func BenchmarkAs_Inlined(b *testing.B) {
    err := makeErrorChain(3)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = checkWithAs(err)
    }
}

func BenchmarkAsType_Inlined(b *testing.B) {
    err := makeErrorChain(3)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = checkWithAsType(err)
    }
}

// 正确性验证
func TestCorrectness(t *testing.T) {
    err := makeErrorChain(3)

    // 旧 API
    var oldTarget *os.PathError
    oldOk := errors.As(err, &oldTarget)

    // 新 API
    newTarget, newOk := errors.AsType[*os.PathError](err)

    if oldOk != newOk {
        t.Fatalf("ok mismatch: old=%v, new=%v", oldOk, newOk)
    }
    if oldOk && oldTarget.Path != newTarget.Path {
        t.Fatalf("path mismatch: old=%v, new=%v", oldTarget.Path, newTarget.Path)
    }
    t.Logf("Both APIs return: ok=%v, path=%s", oldOk, oldTarget.Path)
}
// 测试指令:go test -bench=Benchmark -benchmem -v 

结果(32核CPU)

PS D:\GoLand\errorsbench> go test -bench=Benchmark -benchmem -v        
=== RUN   TestCorrectness
    main_test.go:104: Both APIs return: ok=true, path=/tmp/test.txt
--- PASS: TestCorrectness (0.00s)
goos: windows
goarch: amd64
pkg: errorsbench
cpu: 13th Gen Intel(R) Core(TM) i9-13900HX
BenchmarkAs_Depth1
BenchmarkAs_Depth1-32           14721577                76.50 ns/op            8 B/op          1 allocs/op
BenchmarkAsType_Depth1
BenchmarkAsType_Depth1-32       451059481                2.728 ns/op           0 B/op          0 allocs/op
BenchmarkAs_Depth3
BenchmarkAs_Depth3-32           10217434               115.4 ns/op             8 B/op          1 allocs/op
BenchmarkAsType_Depth3
BenchmarkAsType_Depth3-32       107737664               11.46 ns/op            0 B/op          0 allocs/op
BenchmarkAs_Depth5
BenchmarkAs_Depth5-32            7936659               147.8 ns/op             8 B/op          1 allocs/op
BenchmarkAsType_Depth5
BenchmarkAsType_Depth5-32       70322662                17.79 ns/op            0 B/op          0 allocs/op
BenchmarkAs_Inlined
BenchmarkAs_Inlined-32          10316660               113.7 ns/op             8 B/op          1 allocs/op
BenchmarkAsType_Inlined
BenchmarkAsType_Inlined-32      100000000               11.07 ns/op            0 B/op          0 allocs/op
PASS
ok      errorsbench     12.265s
场景errors.Aserrors.AsType加速比内存差异
Depth176.50 ns2.728 ns28x8B vs 0B
Depth3115.4 ns11.46 ns10x8B vs 0B
Depth5147.8 ns17.79 ns8.3x8B vs 0B
Inlined113.7 ns11.07 ns10x8B vs 0B

核心机制:testing.B 的工作方式

func BenchmarkXxx(b *testing.B) {
    // 准备代码(不计时)
    data := prepare()
    
    b.ResetTimer()  // ← 重置计时器,从这里开始算时间
    
    for i := 0; i < b.N; i++ {  // ← b.N 是框架自动调整的
        // 被测代码(循环执行 b.N 次)
        operation(data)
    }
}

b.N 是什么?

  • 不是固定的,是 Go 测试框架自动调整的
  • 框架会先跑几次,估算时间,然后调整 b.N 让总运行时间 ≈ 1 秒
  • 你的结果里 b.N 就是实际执行次数(如 451059481 次)

逐函数拆解

数据准备:makeErrorChain

func makeErrorChain(depth int) error {
    base := &os.PathError{...}  // 底层错误
    err := error(base)
    for i := 0; i < depth-1; i++ {
        err = fmt.Errorf("layer %d: %w", i, err)  // 用 %w 包装
    }
    return err
}
  • depth=1: PathError(无包装)
  • depth=3: fmt.Errorffmt.ErrorfPathError(两层包装)
  • depth=5: 四层包装 + 底层

测旧 API:benchmarkOldAs

func benchmarkOldAs(b *testing.B, chainDepth int) {
    err := makeErrorChain(chainDepth)  // 准备数据(不计时)
    
    b.ResetTimer()  // ← 从这里开始计时
    
    for i := 0; i < b.N; i++ {
        var target *os.PathError
        if errors.As(err, &target) {  // ← 反射遍历错误链
            _ = target.Path  // ← 防止编译器优化掉整个 if
        }
    }
}

发生了什么:

  1. errors.As 内部用 reflect 遍历错误链
  2. 每层都查类型表、判断可赋值性
  3. 找到匹配后通过指针写回 target
  4. 每次都有反射对象分配(你的结果:8B/1allocs)

测新 API:benchmarkNewAsType

func benchmarkNewAsType(b *testing.B, chainDepth int) {
    err := makeErrorChain(chainDepth)
    
    b.ResetTimer()
    
    for i := 0; i < b.N; i++ {
        if target, ok := errors.AsType[*os.PathError](err); ok {    // ← 泛型错误链
            _ = target.Path
        }
    }
}

发生了什么:

  1. errors.AsType[*os.PathError] 编译期确定类型
  2. 内部直接 err.(*os.PathError) 类型断言
  3. 无反射、无动态遍历、无内存分配
  4. 可被内联(你的 Inlined 测试验证了这点)(内联:把函数调用直接替换成函数体代码,消除调用开销)
func checkWithAsType(err error) bool {
    _, ok := errors.AsType[*os.PathError](err)
    return ok
}

// _, ok := errors.AsType[*os.PathError](err)
// 等价于直接写
// _, ok := err.(*os.PathError)  // 直接展开,无函数调用
func checkWithAs(err error) bool {
    var target *os.PathError
    return errors.As(err, &target)  // 反射调用,要去判断类型,无法直接断言类型展开调用
}

内联测试:checkWithAs vs checkWithAsType

func checkWithAs(err error) bool {
    var target *os.PathError
    return errors.As(err, &target)
}

func BenchmarkAs_Inlined(b *testing.B) {
    err := makeErrorChain(3)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = checkWithAs(err)  // ← 测函数调用 + 内部逻辑
    }
}

设计意图:

  • 把小函数抽出来,看编译器能否内联
  • checkWithAsType 能被内联 → 性能 ≈ 直接写循环
  • checkWithAs 因反射无法内联 → 有函数调用开销

正确性验证:TestCorrectness

func TestCorrectness(t *testing.T) {
    err := makeErrorChain(3)
    
    oldOk := errors.As(err, &oldTarget)
    newOk, newTarget := errors.AsType[*os.PathError](err)
    
    // 确保两个 API 返回相同结果
    if oldOk != newOk || oldTarget.Path != newTarget.Path {
        t.Fatal("mismatch")
    }
}

为什么需要:

  • Benchmark 只测性能,不测正确性
  • 防止"跑得快但结果是错的"情况

执行流程总结

go test -bench=Benchmark -benchmem -v
           ↓
    发现所有 BenchmarkXxx 函数
           ↓
    对每个函数:
        1. 先跑 1 次,估算时间(用于估算1s用时的次数)
        2. 调整 b.N(如从 1 调到 451059481,用时接近1s)
        3. 正式跑 b.N 次,计时
        4. 计算:总时间 / b.N = ns/op
        5. 统计内存分配(-benchmem)
           ↓
    输出结果行:
    BenchmarkAsType_Depth1-32  451059481  2.728 ns/op  0 B/op  0 allocs/op
              ↑                    ↑          ↑          ↑         ↑
           函数名-CPU数           执行次数   每次耗时   内存/次   分配次数/次

关键设计技巧

技巧代码体现目的
b.ResetTimer()准备数据后调用排除构造时间
_ = target.Path使用返回值防止编译器优化掉整个操作
b.N 循环for i := 0; i < b.N; i++让框架决定跑多少次
辅助函数benchmarkOldAs(b, depth)复用逻辑,参数化场景
小函数包装checkWithAs测试内联效果
PREV
[Golang] 1.26版本新GC回收——Green Tea 🍵 Garbage Collector
NEXT
[Golang] 通过errors.AsType的改动反映反射的优化方向

评论(0)

发布评论