10. Go测试与工程实践
10.1 核心知识点(面试必问)
测试命令速查
go test -v ./... # 运行所有测试
go test -run TestAdd # 运行指定测试函数
go test -cover # 查看覆盖率
go test -bench=. -benchmem # 运行基准测试(含内存分配)
go tool pprof cpu.prof # 分析性能文件常见面试题一览
| 问题 | 核心考点 |
|---|---|
| 如何写好单元测试? | 表驱动测试、Table Driven Tests |
| 怎么测试依赖数据库的函数? | Mock(Sqlmock/Gomock)或 Docker容器 |
| 怎么做性能分析? | Benchmark、pprof、火焰图 |
| 项目目录怎么分层? | cmd/internal/pkg (Standard Go Layout) |
10.2 高频面试题详解
Q1: 什么是表驱动测试(Table-Driven Tests)?为什么要用它?
回答要点: Go 官方推荐的测试模式。通过定义一个结构体数组(Slice),包含输入参数、预期输出和测试名称,然后遍历数组运行测试。 优点:
- 代码复用:测试逻辑只写一次。
- 易于扩展:新增测试用例只需添加一行数据。
- 清晰可读:数据与逻辑分离。
代码示例:
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
// 1. 定义测试用例结构
tests := []struct {
name string // 测试用例名称
a, b int // 输入
want int // 预期输出
}{
{"正数相加", 1, 2, 3},
{"负数相加", -1, -2, -3},
{"零值测试", 0, 0, 0},
}
// 2. 遍历执行
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // t.Run 运行子测试
if got := Add(tt.a, tt.b); got != tt.want {
t.Errorf("Add() = %v, want %v", got, tt.want)
}
})
}
}Q2: 如何对依赖网络/数据库的代码进行单元测试?(Mock)
回答要点: 不应该连接真实的数据库或外部API(那叫集成测试)。单元测试应该使用 Mock 技术。
- 接口隔离:业务逻辑依赖接口(Interface),而不是具体实现。
- Mock实现:在测试中注入一个实现了该接口的 Mock 对象。
代码示例:
// 业务代码:依赖接口
type DB interface {
Get(key string) string
}
func GetValue(db DB, key string) string {
v := db.Get(key)
if v == "" {
return "default"
}
return v
}
// ------------------------------------------------
// 测试代码:手写 Mock
type MockDB struct {
Data map[string]string
}
func (m MockDB) Get(key string) string {
return m.Data[key]
}
func TestGetValue(t *testing.T) {
// 注入 Mock 对象
mock := MockDB{Data: map[string]string{"user": "Alice"}}
if got := GetValue(mock, "user"); got != "Alice" {
t.Errorf("Want Alice, got %s", got)
}
if got := GetValue(mock, "unknown"); got != "default" {
t.Errorf("Want default, got %s", got)
}
}进阶:使用
gomock或sqlmock库可以更方便地模拟复杂行为。
Q3: 单元测试 vs 基准测试(Benchmark)?
区别:
- TestXxx (
*testing.T):验证正确性(逻辑对不对)。 - BenchmarkXxx (
*testing.B):验证性能(快不快、内存分配多少)。
Benchmark 写法:
// 目标:测试 append 的性能
func BenchmarkAppend(b *testing.B) {
// b.N 是系统自动调整的循环次数(1, 100, 10000...)
// 直到运行时间足够长
for i := 0; i < b.N; i++ {
var s []int
// 模拟操作
for j := 0; j < 1000; j++ {
s = append(s, j)
}
}
}运行命令:
go test -bench=. -benchmem
# 输出示例:
# BenchmarkAppend-8 100000 1560 ns/op 5020 B/op 10 allocs/op
# ↑次数 ↑每次耗时 ↑每次内存 ↑每次分配次数Q4: Go 项目的标准目录结构是什么?
虽然 Go 官方没有强制规定,但社区公认标准(Go Standard Project Layout):
my-project/
├── cmd/ # main 函数入口
│ └── server/
│ └── main.go
├── internal/ # 私有应用代码(外部项目无法导入!)
│ ├── handler/ # HTTP 处理层
│ ├── service/ # 业务逻辑层
│ └── model/ # 数据模型
├── pkg/ # 公共库代码(允许被外部项目引用)
│ └── utils/
├── api/ # OpenAPI/Swagger/Proto 定义
├── configs/ # 配置文件
├── go.mod
└── go.sum
重点追问:internal 和 pkg 的区别?
internal/:编译器强制限制,只有当前 module 及其子目录可以 import,外部项目引用会报错。用于保护核心业务逻辑。pkg/:开放的工具库,任何项目都可以 import。
Q5: Go Modules (go.mod) 常见命令
| 命令 | 作用 |
|---|---|
go mod init <name> | 初始化新模块 |
go mod tidy | 最常用!自动添加缺少的依赖,删除没用的依赖 |
go mod vendor | 将依赖包复制到项目下的 vendor 目录 |
go get -u ./... | 更新所有依赖到最新版(慎用) |
go mod why <pkg> | 查看为什么要依赖某个包 |
10.3 逃逸分析 (Escape Analysis)
这其实属于编译器优化,但常在性能优化面试中问到。
定义:编译器决定变量应该分配在**栈(Stack)还是堆(Heap)**上的过程。
- 栈:函数结束自动释放,快,无GC压力。
- 堆:需要GC回收,相对慢,有GC压力。
常见逃逸场景(分配到堆):
- 返回局部变量指针:函数返回了内部变量的地址(必须在函数外存活)。
- interface{} 动态类型:如
fmt.Println(a),参数是interface{},难以确定具体类型大小。 - 栈空间不足:对象过大(如
make([]int, 1000000))。 - 闭包引用:闭包由于上下文引用,导致变量逃逸。
查看逃逸命令:
go build -gcflags="-m -l" main.go
# 输出:
# ./main.go:10: moved to heap: x
# ./main.go:12: func literal escapes to heap