Go 从 0 到精通 · 第 32 课:Benchmark 与性能测试
学习定位:这是整套 Go 教程的第 32 课,也是阶段五(并发与工程阶段)的第八课。
前置要求:已经完成第 31 课,掌握了 Go 的 testing 包、单元测试和表驱动测试。
本课目标:掌握 Go 基准测试的写法,理解 b.N 的工作机制,学会用 go test -bench 运行基准测试,能对比不同实现的性能差异,了解基准测试的常见误区。
1. 本课你要解决的核心问题
上一课你学会了测试——确认代码"对不对"。这一课解决另一个问题:代码"快不快"。
你写了两种字符串拼接方式,哪个更快?你把 map 换成了 slice,性能提升了多少?你优化了一个函数,到底快了还是慢了?
靠猜不行,得用数据说话。
Go 内置了基准测试(Benchmark)功能。跟单元测试一样,不需要装任何第三方工具,写一个函数、跑一条命令,就能得到精确的性能数据。
这一课讲清楚以下问题:
基准测试函数怎么写
b.N 是什么,Go 怎么自动决定跑多少次
怎么运行基准测试、怎么看结果
怎么对比两种实现的性能差异
怎么测内存分配
基准测试有哪些常见的坑
2. 最简单的基准测试
2.1 被测函数
沿用上一课的 Reverse 函数:
1 2 3 4 5 6 7 8 9 10 package stringutilfunc Reverse (s string ) string { runes := []rune (s) for i, j := 0 , len (runes)-1 ; i < j; i, j = i+1 , j-1 { runes[i], runes[j] = runes[j], runes[i] } return string (runes) }
2.2 写基准测试
在 stringutil_test.go 中加一个函数:
1 2 3 4 5 6 7 8 9 package stringutilimport "testing" func BenchmarkReverse (b *testing.B) { for i := 0 ; i < b.N; i++ { Reverse("hello, world" ) } }
就这么简单。跟单元测试对比:
单元测试
基准测试
函数前缀
Test
Benchmark
参数类型
*testing.T
*testing.B
核心逻辑
断言结果对不对
循环 b.N 次测性能
2.3 运行基准测试
输出:
1 2 3 4 5 6 7 goos : windowsgoarch : amd64pkg : stringutilcpu : 12 th Gen Intel(R) Core(TM) i7-12700 HBenchmarkReverse -20 8234567 145 .2 ns/opPASS ok stringutil 1 .345 s
2.4 怎么读结果
1 2 3 4 5 6 BenchmarkReverse-20 8234567 145.2 ns/op │ │ │ │ │ │ │ └── 每次操作耗时 145.2 纳秒 │ │ └── 总共运行了 8234567 次 │ └── 使用了 20 个 CPU 核心 └── 函数名
核心指标是 ns/op ——每次操作耗时多少纳秒。这个数字越小,性能越好。
3. 理解 b.N
3.1 b.N 是什么
b.N 是 Go 自动决定的循环次数。你不需要手动设置。
Go 的做法是:先用一个小的 N 跑一遍(比如 1 次),看耗时够不够。不够就加大 N(10、100、1000…),直到总耗时达到一个稳定的基准(默认约 1 秒)。
1 2 3 4 5 6 7 8 9 10 func BenchmarkReverse (b *testing.B) { for i := 0 ; i < b.N; i++ { Reverse("hello, world" ) } }
3.2 为什么要这样
如果固定跑 100 次,对于一个耗时 1 微秒的函数来说太少了,误差很大。对于一个耗时 1 秒的函数来说太多了,要等好几分钟。
Go 自动调整 N,确保:
耗时短的函数多跑几轮,减小误差
耗时长的函数少跑几轮,节省时间
3.3 绝对不要这样写
1 2 3 4 5 6 func BenchmarkReverse (b *testing.B) { for i := 0 ; i < 10000 ; i++ { Reverse("hello, world" ) } }
如果你用固定数字而不是 b.N,Go 会认为"这个函数只需要跑一次",然后给你一个没有意义的结果。
4. 运行基准测试的常用命令
4.1 基本命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 go test -bench . go test -bench . ./... go test -bench BenchmarkReverse go test -bench "Benchmark(Reverse|Concat)" go test -bench . -benchmem go test -bench . -benchtime 3s go test -bench . -benchtime 100x go test -bench . -count 5 go test -bench . -run ^$
4.2 最常用的组合
1 2 3 4 5 6 7 8 go test -bench . -benchmem -count 3 go test -bench BenchmarkReverse -run ^$ -benchmem go test -bench . -benchtime 5s -benchmem
4.3 关于 -run ^$
go test -bench . 默认会同时运行单元测试。如果你只想跑基准测试,加 -run ^$(匹配一个不存在的测试名),这样单元测试全部被跳过。
5. 对比不同实现的性能
基准测试最有价值的场景就是对比 。
5.1 字符串拼接的三种方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package stringutilimport ( "fmt" "strings" ) func ConcatPlus (strs []string ) string { result := "" for _, s := range strs { result += s } return result } func ConcatSprintf (strs []string ) string { result := "" for _, s := range strs { result = fmt.Sprintf("%s%s" , result, s) } return result } func ConcatBuilder (strs []string ) string { var builder strings.Builder for _, s := range strs { builder.WriteString(s) } return builder.String() } func ConcatJoin (strs []string ) string { return strings.Join(strs, "" ) }
5.2 写基准测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package stringutilimport ( "strings" "testing" ) func makeStrings (n int ) []string { strs := make ([]string , n) for i := range strs { strs[i] = "hello" } return strs } func BenchmarkConcatPlus (b *testing.B) { strs := makeStrings(100 ) b.ResetTimer() for i := 0 ; i < b.N; i++ { ConcatPlus(strs) } } func BenchmarkConcatSprintf (b *testing.B) { strs := makeStrings(100 ) b.ResetTimer() for i := 0 ; i < b.N; i++ { ConcatSprintf(strs) } } func BenchmarkConcatBuilder (b *testing.B) { strs := makeStrings(100 ) b.ResetTimer() for i := 0 ; i < b.N; i++ { ConcatBuilder(strs) } } func BenchmarkConcatJoin (b *testing.B) { strs := makeStrings(100 ) b.ResetTimer() for i := 0 ; i < b.N; i++ { ConcatJoin(strs) } }
5.3 运行并对比
1 go test -bench BenchmarkConcat -benchmem -run ^$
输出(示意):
1 2 3 4 BenchmarkConcatPlus -20 18534 64521 ns/op 53336 B/op 99 allocs/opBenchmarkConcatSprintf -20 8245 145234 ns/op 106824 B/op 298 allocs/opBenchmarkConcatBuilder -20 325678 3682 ns/op 2544 B/op 8 allocs/opBenchmarkConcatJoin -20 412345 2916 ns/op 512 B/op 1 allocs/op
5.4 怎么读对比结果
1 2 3 4 5 次数 耗时 内存 分配次数 ConcatPlus 18534 64521 ns/op 53336 B/op 99 allocs/op ConcatSprintf 8245 145234 ns/op 106824 B/op 298 allocs/op ConcatBuilder 325678 3682 ns/op 2544 B/op 8 allocs/op ConcatJoin 412345 2916 ns/op 512 B/op 1 allocs/op
指标
含义
ns/op
每次操作耗时(纳秒)。越小越快
B/op
每次操作分配的内存(字节)。越小越省内存
allocs/op
每次操作的内存分配次数。越少越好
结论一目了然:
strings.Join 最快,内存分配最少
strings.Builder 紧随其后
+ 号拼接慢了 20 倍,内存分配多了 100 倍
fmt.Sprintf 最慢
这就是基准测试的价值——用数据说话,不靠猜测。
6. b.ResetTimer、b.StopTimer、b.StartTimer
6.1 b.ResetTimer()
用于排除准备工作的耗时 。放在准备代码之后、循环之前:
1 2 3 4 5 6 7 8 9 10 func BenchmarkProcess (b *testing.B) { data := loadLargeFile() b.ResetTimer() for i := 0 ; i < b.N; i++ { process(data) } }
不加 b.ResetTimer(),2 秒的准备时间会被算进基准测试结果,导致数据不准。
6.2 b.StopTimer() 和 b.StartTimer()
用于在循环内部排除非测试代码的耗时 :
1 2 3 4 5 6 7 8 9 10 11 12 13 func BenchmarkInsert (b *testing.B) { for i := 0 ; i < b.N; i++ { b.StopTimer() db := setupTestDB() b.StartTimer() db.Insert("key" , "value" ) b.StopTimer() db.Cleanup() b.StartTimer() } }
注意 :StopTimer/StartTimer 本身有开销。如果被测代码执行很快(几十纳秒),频繁调用它们会导致结果不准确。能用 b.ResetTimer() 解决的就别用 StopTimer/StartTimer。
6.3 选择建议
场景
用什么
循环前有一次性准备工作
b.ResetTimer()
每次循环内部有准备/清理工作
b.StopTimer() + b.StartTimer()
没有额外准备工作
什么都不用加
7. 内存分配基准测试
7.1 用 -benchmem 看内存
1 go test -bench . -benchmem
输出中会多两列:
1 2 3 4 BenchmarkReverse-20 8234567 145.2 ns/op 48 B/op 2 allocs/op │ │ │ └── 每次操作分配了 2 次内存 └── 每次操作分配了 48 字节
7.2 用 b.ReportAllocs()
也可以在代码中强制报告内存,不需要加 -benchmem 参数:
1 2 3 4 5 6 7 func BenchmarkReverse (b *testing.B) { b.ReportAllocs() for i := 0 ; i < b.N; i++ { Reverse("hello, world" ) } }
7.3 为什么要关注内存分配
Go 的垃圾回收器(GC)需要处理所有分配的内存。分配越多:
GC 压力越大
程序可能出现短暂停顿
总体性能下降
很多性能优化的核心就是减少内存分配 。
7.4 实例:减少内存分配的优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func ReverseV1 (s string ) string { runes := []rune (s) for i, j := 0 , len (runes)-1 ; i < j; i, j = i+1 , j-1 { runes[i], runes[j] = runes[j], runes[i] } return string (runes) } func ReverseV2 (s string ) string { isASCII := true for i := 0 ; i < len (s); i++ { if s[i] > 127 { isASCII = false break } } if isASCII { b := make ([]byte , len (s)) for i, j := 0 , len (s)-1 ; i <= j; i, j = i+1 , j-1 { b[i], b[j] = s[j], s[i] } return string (b) } runes := []rune (s) for i, j := 0 , len (runes)-1 ; i < j; i, j = i+1 , j-1 { runes[i], runes[j] = runes[j], runes[i] } return string (runes) }
基准测试对比:
1 2 3 4 5 6 7 8 9 10 11 12 13 func BenchmarkReverseV1 (b *testing.B) { b.ReportAllocs() for i := 0 ; i < b.N; i++ { ReverseV1("hello, world!" ) } } func BenchmarkReverseV2 (b *testing.B) { b.ReportAllocs() for i := 0 ; i < b.N; i++ { ReverseV2("hello, world!" ) } }
结果(示意):
1 2 BenchmarkReverseV1 -20 5432198 221 .3 ns/op 80 B/op 2 allocs/opBenchmarkReverseV2 -20 9876543 121 .5 ns/op 32 B/op 1 allocs/op
V2 对纯 ASCII 字符串快了近一倍,内存分配也少了一半。
8. 子基准测试
8.1 用 b.Run 测试不同规模
跟单元测试的 t.Run 一样,基准测试也支持 b.Run 创建子基准测试。最常见的用法是测试不同输入规模下的性能 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func BenchmarkReverse (b *testing.B) { sizes := []struct { name string input string }{ {"短字符串" , "hello" }, {"中等字符串" , "hello, world! this is a benchmark test" }, {"长字符串" , strings.Repeat("abcdefghij" , 100 )}, } for _, s := range sizes { b.Run(s.name, func (b *testing.B) { for i := 0 ; i < b.N; i++ { Reverse(s.input) } }) } }
输出:
1 2 3 BenchmarkReverse /短字符串-20 12345678 97 .3 ns/opBenchmarkReverse /中等字符串-20 4567890 263 .1 ns/opBenchmarkReverse /长字符串-20 123456 9712 .0 ns/op
一眼看出性能跟输入长度的关系。
8.2 用 b.Run 对比多种实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func BenchmarkConcat (b *testing.B) { strs := makeStrings(100 ) b.Run("Plus" , func (b *testing.B) { for i := 0 ; i < b.N; i++ { ConcatPlus(strs) } }) b.Run("Builder" , func (b *testing.B) { for i := 0 ; i < b.N; i++ { ConcatBuilder(strs) } }) b.Run("Join" , func (b *testing.B) { for i := 0 ; i < b.N; i++ { ConcatJoin(strs) } }) }
输出:
1 2 3 BenchmarkConcat /Plus-20 18534 64521 ns/opBenchmarkConcat /Builder-20 325678 3682 ns/opBenchmarkConcat /Join-20 412345 2916 ns/op
8.3 综合:不同实现 × 不同规模
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 func BenchmarkConcatMatrix (b *testing.B) { sizes := []int {10 , 100 , 1000 } for _, size := range sizes { strs := makeStrings(size) b.Run(fmt.Sprintf("Plus/%d" , size), func (b *testing.B) { for i := 0 ; i < b.N; i++ { ConcatPlus(strs) } }) b.Run(fmt.Sprintf("Builder/%d" , size), func (b *testing.B) { for i := 0 ; i < b.N; i++ { ConcatBuilder(strs) } }) b.Run(fmt.Sprintf("Join/%d" , size), func (b *testing.B) { for i := 0 ; i < b.N; i++ { ConcatJoin(strs) } }) } }
输出(示意):
1 2 3 4 5 6 7 8 9 BenchmarkConcatMatrix /Plus/10 -20 567890 2105 ns/opBenchmarkConcatMatrix /Builder/10 -20 2345678 511 ns/opBenchmarkConcatMatrix /Join/10 -20 3456789 345 ns/opBenchmarkConcatMatrix /Plus/100 -20 18534 64521 ns/opBenchmarkConcatMatrix /Builder/100 -20 325678 3682 ns/opBenchmarkConcatMatrix /Join/100 -20 412345 2916 ns/opBenchmarkConcatMatrix /Plus/1000 -20 1234 9654321 ns/opBenchmarkConcatMatrix /Builder/1000 -20 45678 26345 ns/opBenchmarkConcatMatrix /Join/1000 -20 56789 21234 ns/op
可以看到:+ 号拼接在数据量增大时性能急剧下降(O(n²)),而 Builder 和 Join 基本线性增长。
9. 完整实战:map vs slice 查找性能
9.1 两种查找方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package lookupfunc SliceLookup (data []int , target int ) bool { for _, v := range data { if v == target { return true } } return false } func MapLookup (data map [int ]bool , target int ) bool { return data[target] }
9.2 基准测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package lookupimport ( "testing" ) func makeSlice (n int ) []int { s := make ([]int , n) for i := range s { s[i] = i } return s } func makeMap (n int ) map [int ]bool { m := make (map [int ]bool , n) for i := 0 ; i < n; i++ { m[i] = true } return m } func BenchmarkLookup (b *testing.B) { sizes := []int {10 , 100 , 1000 , 10000 } for _, size := range sizes { sliceData := makeSlice(size) mapData := makeMap(size) target := size - 1 b.Run(fmt.Sprintf("Slice/%d" , size), func (b *testing.B) { for i := 0 ; i < b.N; i++ { SliceLookup(sliceData, target) } }) b.Run(fmt.Sprintf("Map/%d" , size), func (b *testing.B) { for i := 0 ; i < b.N; i++ { MapLookup(mapData, target) } }) } }
9.3 预期结果分析
1 2 3 4 5 6 7 8 BenchmarkLookup /Slice/10 -20 50000000 24 ns/opBenchmarkLookup /Map/10 -20 30000000 40 ns/opBenchmarkLookup /Slice/100 -20 5000000 288 ns/opBenchmarkLookup /Map/100 -20 25000000 45 ns/opBenchmarkLookup /Slice/1000 -20 500000 2876 ns/opBenchmarkLookup /Map/1000 -20 20000000 51 ns/opBenchmarkLookup /Slice/10000 -20 50000 28934 ns/opBenchmarkLookup /Map/10000 -20 18000000 58 ns/op
结论:
数据量小(10 个以下) :slice 比 map 快(没有 hash 计算的开销)
数据量大(100+) :map 远快于 slice(O(1) vs O(n))
拐点大约在 20~30 个元素
这种数据对日常决策很有用:小集合用 slice,大集合用 map。
10. 常见坑总结
10.1 编译器优化掉了你的代码
这是基准测试中最常见也最隐蔽 的坑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func BenchmarkReverse (b *testing.B) { for i := 0 ; i < b.N; i++ { Reverse("hello" ) } } var result string func BenchmarkReverse (b *testing.B) { var r string for i := 0 ; i < b.N; i++ { r = Reverse("hello" ) } result = r }
Go 编译器很聪明。如果它发现一个函数的返回值没被使用,在某些情况下会优化掉整个调用。把结果赋给包级变量可以阻止这种优化。
实际中 ,Go 目前的编译器在大多数情况下不会优化掉有副作用的函数调用。但养成这个习惯是好的,尤其是测试纯计算函数时。
10.2 循环内部做了太多不相关的事
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func BenchmarkProcess (b *testing.B) { for i := 0 ; i < b.N; i++ { data := loadData() Process(data) } } func BenchmarkProcess (b *testing.B) { data := loadData() b.ResetTimer() for i := 0 ; i < b.N; i++ { Process(data) } }
10.3 忘了 b.ResetTimer()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func BenchmarkDB (b *testing.B) { db := connectDatabase() defer db.Close() for i := 0 ; i < b.N; i++ { db.Query("SELECT 1" ) } } func BenchmarkDB (b *testing.B) { db := connectDatabase() defer db.Close() b.ResetTimer() for i := 0 ; i < b.N; i++ { db.Query("SELECT 1" ) } }
10.4 用固定数字代替 b.N
1 2 3 4 5 6 func BenchmarkReverse (b *testing.B) { for i := 0 ; i < 10000 ; i++ { Reverse("hello" ) } }
上面已经说过,不再重复。记住:循环条件永远是 i < b.N。
10.5 基准测试结果不稳定
同一个基准测试,每次跑出来的结果可能不一样。这是正常的。减小波动的方法:
1 2 3 4 5 6 7 go test -bench . -count 5 go test -bench . -benchtime 5s
不要在做其他事情的电脑上跑基准测试 。浏览器、音乐播放器、IDE 都会影响结果。
10.6 只看 ns/op,忽略 allocs/op
1 go test -bench . -benchmem
有时候两种实现的 ns/op 差不多,但 allocs/op 差很多。内存分配多的在高并发场景下会因 GC 压力变慢。永远加 -benchmem。
11. 实用技巧
11.1 benchstat 工具
Go 官方提供了 benchstat 工具,用于统计分析 基准测试结果:
1 2 3 4 5 6 7 8 9 10 11 go install golang.org/x/perf/cmd/benchstat@latest go test -bench . -benchmem -count 5 > old.txt go test -bench . -benchmem -count 5 > new.txt benchstat old.txt new.txt
输出(示意):
1 2 3 4 5 6 7 8 name old time/op new time/op deltaReverse -20 145 ns ± 2 % 122 ns ± 1 % -15 .86 % (p=0 .008 n=5 +5 )name old alloc/op new alloc/op deltaReverse -20 48 .0 B ± 0 % 32 .0 B ± 0 % -33 .33 % (p=0 .008 n=5 +5 )name old allocs/op new allocs/op deltaReverse -20 2 .00 ± 0 % 1 .00 ± 0 % -50 .00 % (p=0 .008 n=5 +5 )
benchstat 会计算标准差、p 值,告诉你性能变化是否统计显著 。±2% 是波动范围,p=0.008 说明变化是真实的(不是噪音)。
11.2 b.ReportMetric() 自定义指标
1 2 3 4 5 6 7 8 9 10 11 12 13 func BenchmarkSort (b *testing.B) { data := makeRandomSlice(10000 ) b.ResetTimer() for i := 0 ; i < b.N; i++ { sorted := make ([]int , len (data)) copy (sorted, data) sort.Ints(sorted) } b.ReportMetric(float64 (len (data)), "items/op" ) }
输出会多一列 10000 items/op,让你知道每次操作处理了多少数据。
11.3 testing.Short() 跳过慢的基准测试
1 2 3 4 5 6 func BenchmarkExpensive (b *testing.B) { if testing.Short() { b.Skip("跳过耗时基准测试" ) } }
12. 本课练习
练习 1:切片追加的性能
对比以下三种构建切片的方式:
直接 append(不预分配容量)
make([]int, 0, n) 预分配容量后 append
make([]int, n) 后用索引赋值
用基准测试对比它们在 n=100、n=1000、n=10000 时的性能差异。
练习 2:map 初始化的影响
对比以下两种 map 使用方式:
make(map[string]int) 不指定容量
make(map[string]int, n) 预分配容量
向 map 中插入 1000 个键值对,用基准测试对比性能和内存分配。
练习 3:字符串查找
对比以下几种判断字符串是否包含子串的方式:
strings.Contains
strings.Index >= 0
手写循环匹配
在短字符串(20 字符)和长字符串(10000 字符)上分别测试。
练习 4:JSON 序列化
创建一个包含 10 个字段的结构体,用基准测试对比:
json.Marshal
手动拼接 JSON 字符串
测量性能差异和内存分配差异。
练习 5:完整的优化流程
写一个函数:接收一个字符串切片,返回所有字符串的去重结果
先写最简单的实现(两层循环)
再写优化实现(用 map 去重)
用基准测试对比两种实现
用 benchstat 生成对比报告
13. 自测题
13.1 概念题
基准测试函数的命名规则和参数类型是什么?
b.N 是什么?为什么不能用固定数字代替它?
b.ResetTimer() 的作用是什么?什么时候需要用?
-benchmem 参数输出的三列数据分别是什么意思?
为什么说基准测试中编译器优化是一个坑?怎么避免?
b.Run 创建子基准测试有什么好处?
什么时候应该用 b.StopTimer/b.StartTimer?有什么注意事项?
为什么基准测试结果每次都不完全一样?怎么让结果更可靠?
13.2 代码阅读题
以下基准测试有两个问题,找出来:
1 2 3 4 5 6 7 8 func BenchmarkProcess (b *testing.B) { data := loadLargeDataset() for i := 0 ; i < 10000 ; i++ { result := Process(data) fmt.Println(result) } }
点击查看答案
问题 1:用了固定数字 10000 而不是 b.N 。循环条件应该是 i < b.N,否则 Go 无法正确测量每次操作的耗时。
问题 2:没有用 b.ResetTimer() 。loadLargeDataset() 耗时 3 秒,这个时间会被计入基准测试结果,导致 ns/op 严重偏高。
还有一个额外问题:fmt.Println(result) 会做 I/O 操作,这个耗时也被算进去了。如果目的是防止编译器优化,应该赋给包级变量,而不是打印。
修复后:
1 2 3 4 5 6 7 8 9 10 11 12 var benchResult int func BenchmarkProcess (b *testing.B) { data := loadLargeDataset() b.ResetTimer() var r int for i := 0 ; i < b.N; i++ { r = Process(data) } benchResult = r }
14. 本课总结
这一课你学到了 Go 基准测试的核心内容。
知识点
要点
基本写法
BenchmarkXxx(b *testing.B),循环 b.N 次
运行命令
go test -bench . -benchmem
核心指标
ns/op(耗时)、B/op(内存)、allocs/op(分配次数)
计时控制
b.ResetTimer()、b.StopTimer()、b.StartTimer()
子基准测试
b.Run 对比不同规模、不同实现
分析工具
benchstat 统计对比、-count 多次运行
最重要的三件事:
循环条件永远是 i < b.N——Go 自动调整 N 来获得稳定结果
永远加 -benchmem——内存分配往往比执行时间更能说明问题
优化前先跑基准测试,优化后再跑一次对比——用数据驱动决策,不靠猜测
15. 下一课预告
到这里你已经会写测试、会跑基准测试了。下一步该学怎么组织一个完整的 Go 项目。
下一课:项目结构与工程组织
会重点讲:
Go 项目的常见目录结构
包怎么拆分才合理
配置、日志和错误包装的标准做法
internal 目录的用途
从零搭建一个结构清晰的小型工程项目
学完下一课,你就不再是"能写 Go 代码",而是"能组织 Go 项目"了。