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),包含输入参数、预期输出和测试名称,然后遍历数组运行测试。 优点:

  1. 代码复用:测试逻辑只写一次。
  2. 易于扩展:新增测试用例只需添加一行数据。
  3. 清晰可读:数据与逻辑分离。

代码示例

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 技术。

  1. 接口隔离:业务逻辑依赖接口(Interface),而不是具体实现。
  2. 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)
    }
}

进阶:使用 gomocksqlmock 库可以更方便地模拟复杂行为。


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

重点追问:internalpkg 的区别?

  • 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压力。

常见逃逸场景(分配到堆)

  1. 返回局部变量指针:函数返回了内部变量的地址(必须在函数外存活)。
  2. interface{} 动态类型:如 fmt.Println(a),参数是 interface{},难以确定具体类型大小。
  3. 栈空间不足:对象过大(如 make([]int, 1000000))。
  4. 闭包引用:闭包由于上下文引用,导致变量逃逸。

查看逃逸命令

go build -gcflags="-m -l" main.go
# 输出:
# ./main.go:10: moved to heap: x
# ./main.go:12: func literal escapes to heap