Skip to content

Go 后端面试复盘:语言、并发与服务实践 · 21. 测试、基准测试与性能排查至22. Go 性能优化 ​

返回分篇目录

前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。

21. 测试、基准测试与性能排查 ​

21.1 Go 单元测试基本结构是什么? ​

go
func TestAdd(t *testing.T) {
	got := Add(1, 2)
	want := 3
	if got != want {
		t.Fatalf("Add()=%d, want=%d", got, want)
	}
}

测试文件以 _test.go 结尾,测试函数以 Test 开头。

21.2 什么是表驱动测试? ​

go
func TestParse(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		want    int
		wantErr bool
	}{
		{name: "valid", input: "12", want: 12},
		{name: "invalid", input: "x", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := Parse(tt.input)
			if (err != nil) != tt.wantErr {
				t.Fatalf("err=%v, wantErr=%v", err, tt.wantErr)
			}
			if !tt.wantErr && got != tt.want {
				t.Fatalf("got=%d, want=%d", got, tt.want)
			}
		})
	}

优点是用例结构统一、易扩展、可独立运行子测试。

21.3 测试并行有什么注意事项? ​

使用 t.Parallel() 时:

  • 避免共享可变全局状态。
  • 不要争用固定端口或同一数据库记录。
  • 循环变量要确认版本语义并保持写法清晰。
  • 临时文件使用 t.TempDir()。
  • 环境变量变更与并行测试可能冲突。

21.4 Benchmark 怎么写? ​

go
func BenchmarkEncode(b *testing.B) {
	input := prepare()
	b.ReportAllocs()
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		_, _ = Encode(input)
	}
}

执行:

bash
go test -bench=. -benchmem ./...

关注:

  • ns/op
  • B/op
  • allocs/op

必须避免编译器把结果完全优化掉,也要把准备工作排除在计时之外。

21.5 如何比较两次 benchmark? ​

不要只凭单次结果。建议:

  1. 在稳定机器上多次运行。
  2. 保存 before/after 输出。
  3. 使用统计对比工具分析。
  4. 同时观察延迟、分配和吞吐。
  5. 确认输入和环境一致。

微基准变快不代表整条业务链路一定变快。

21.6 Fuzz Testing 是什么? ​

模糊测试自动生成大量输入,寻找 panic、崩溃、不变量破坏等问题。

go
func FuzzParse(f *testing.F) {
	f.Add("123")
	f.Fuzz(func(t *testing.T, s string) {
		_, _ = Parse(s)
	})
}

适合解析器、协议、编码解码、安全边界。

21.7 pprof 有哪些 profile? ​

  • CPU:CPU 时间花在哪里。
  • heap:当前存活对象或累计分配。
  • goroutine:goroutine 堆栈。
  • mutex:锁竞争等待。
  • block:阻塞事件。
  • allocs:累计分配。
  • threadcreate:线程创建。

分析时区分:

  • flat:函数自身消耗。
  • cum:函数及其调用链累计消耗。

21.8 CPU profile 怎么分析? ​

流程:

  1. 在代表性负载下采样。
  2. 查看 top 找主要 CPU 消耗。
  3. 查看调用图和 source。
  4. 判断是业务计算、序列化、锁、自旋、GC 还是系统调用。
  5. 针对热点修改。
  6. 使用相同条件复测。

不要只优化排名靠前但总占比很低的函数。

21.9 heap profile 中 inuse 和 alloc 的区别? ​

  • inuse_space:当前仍存活的内存,适合看内存占用。
  • inuse_objects:当前存活对象数量。
  • alloc_space:运行期间累计分配字节,适合看分配压力。
  • alloc_objects:累计分配对象数。

某函数累计分配很高但存活很低,可能造成 GC CPU 压力;存活持续增长则可能是缓存、积压或泄漏。

21.10 trace 能看什么? ​

Go execution trace 可观察:

  • goroutine 调度和阻塞。
  • 网络等待。
  • syscall。
  • GC 事件。
  • P 的利用率。
  • 用户标注的 task/region。

适合分析“CPU 不高但延迟很高”“goroutine 大量等待”“调度不均”等问题。trace 数据较大,采集窗口应受控。

21.11 如何建立可观测性? ​

三大支柱:

  • Metrics:聚合趋势和告警。
  • Logs:离散事件和上下文。
  • Traces:跨服务请求链路。

推荐服务指标:

  • 请求量、错误率、延迟分位数。
  • 并发数、队列长度。
  • 连接池使用率和等待。
  • goroutine、堆、GC CPU。
  • 下游调用延迟和错误。

避免高基数 label,例如直接用 user ID、订单 ID 做 metrics 标签。


22. Go 性能优化 ​

22.1 性能优化的正确顺序是什么? ​

  1. 明确目标:吞吐、P99、内存、CPU 还是成本。
  2. 建立可复现基线。
  3. profile 找主要瓶颈。
  4. 优化最大热点。
  5. benchmark 和集成压测验证。
  6. 观察正确性和可维护性。
  7. 上线灰度和监控。

没有数据的优化通常只是猜测。

22.2 常见 CPU 优化方向有哪些? ​

  • 降低算法复杂度。
  • 避免重复计算。
  • 批处理减少函数/网络/锁开销。
  • 使用更合适的数据结构。
  • 减少序列化和格式转换。
  • 改善数据局部性。
  • 降低锁竞争。
  • 缓存真正高成本且结果稳定的计算。

算法和架构层优化通常比语法微优化收益大。

22.3 常见内存优化方向有哪些? ​

  • 减少不必要分配。
  • 预分配 slice/map/buffer。
  • 流式处理大数据。
  • 控制缓存、队列和批次上限。
  • 避免小切片持有大数组。
  • 使用紧凑结构和更合理字段顺序。
  • 避免长期保留超大 buffer。
  • 按需使用 sync.Pool。

22.4 如何优化字符串拼接? ​

go
var b strings.Builder
b.Grow(estimated)
for _, part := range parts {
	b.WriteString(part)
}
result := b.String()

少量固定字符串用 + 通常足够清晰,编译器也可能优化。只有循环或热点中大量拼接才需要 Builder。

22.5 并发越高性能越好吗? ​

不是。过高并发会导致:

  • 调度开销。
  • 锁竞争。
  • cache miss。
  • 下游连接池等待。
  • 队列堆积。
  • 内存增长。
  • 超时雪崩。

应使用有界并发,把并发度与 CPU、下游容量和任务特性匹配。

22.6 什么是假共享? ​

不同 goroutine 修改逻辑上不同的变量,但变量位于同一 CPU cache line,多个核心反复使缓存失效,性能下降。

高频原子计数器、分片状态中可能出现。解决方案包括分片、本地聚合、调整数据布局。padding 属于低层优化,必须用 benchmark 证明。

22.7 锁优化有哪些方法? ​

  • 缩小临界区。
  • 把慢 I/O 移到锁外。
  • 降低共享状态。
  • 分片锁。
  • 使用不可变快照。
  • 批量更新。
  • 单所有者 goroutine。
  • 读写锁仅在合适负载使用。

不要盲目用无锁结构;复杂无锁代码更难证明正确,并可能因原子重试产生高 CPU。

22.8 对象池为什么可能适得其反? ​

  • 池管理本身有成本。
  • 对象未重置导致数据污染。
  • 大对象进入池导致常驻内存过高。
  • 降低代码可读性。
  • GC 和编译器进步后收益可能很小。

只有 profile 显示该类对象分配是明确热点,且对象大小适中、复用频繁时再引入。

22.9 如何优化 API P99? ​

优先查:

  • 下游长尾和重试放大。
  • 连接池等待。
  • 队列排队。
  • 锁竞争。
  • GC assist。
  • 大对象分配。
  • 慢 SQL。
  • DNS/TLS/建连。
  • 日志或序列化阻塞。
  • 超时层级不合理。

平均延迟正常并不代表 P99 正常。


继续阅读