CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Go 语言入门/课时/测试与基准
RawBlame
课时目录12 节

Go 语言入门

  1. 01Go 是什么
  2. 02基础语法与类型
  3. 03数组、切片与 map
  4. 04结构体与接口
  5. 05模块与包管理
  6. 06并发模型
  7. 07错误处理
  8. 08泛型入门
  9. 09I/O 与 CLI
  10. 10Web 与数据库
  11. 11测试与基准
  12. 12综合练习

测试与基准

testing 包、table-driven、httptest、benchmarks。

30 分钟

测试与基准

这一课你将能做到

  1. 写出表驱动测试(table-driven tests),并用 t.Run 组织子测试、用 -run 精准过滤
  2. 使用 go test -v / -cover / -race 等常用参数定位问题与查看覆盖率
  3. 编写基准测试(benchmark),读懂 ns/op 与内存分配数据;用 httptest 测 HTTP handler

为什么要学

Go 把测试做进了工具链:不需要 JUnit/pytest 这类三方框架,新建 _test.go 文件、写 TestXxx 函数、执行 go test,覆盖率、基准、竞态检测全部内置。

更重要的是 Go 社区有一套高度统一的测试审美——表驱动测试:把「用例数据」排成表格,循环跑断言。打开任何 Go 标准库源码,测试都是这个写法。学会它 = 获得读懂整个 Go 生态测试代码的通行证。面试中「说说表驱动测试」「怎么测 HTTP」也是 Go 岗位的固定题。

先建立图像

项目结构(测试文件与被测代码同包同目录):
calc/
├── go.mod
├── calc.go        ← 被测代码 package calc
└── calc_test.go   ← 测试代码 package calc(同名包 + _test.go 后缀)

三种测试函数:
func TestXxx(t *testing.T)   {}   ← 单元测试:go test
func BenchmarkXxx(b *testing.B) {} ← 性能基准:go test -bench=.
func ExampleXxx()            {}   ← 示例文档:go test 校验输出注释

用例表格(表驱动的心脏):
┌──────────┬────┬────┬──────┬──────────┐
│ name     │ a  │ b  │ want │ wantErr  │
├──────────┼────┼────┼──────┼──────────┤
│ 正常除法 │ 10 │ 2  │ 5    │ nil      │
│ 除零     │ 1  │ 0  │ 0    │ ErrZero  │
└──────────┴────┴────┴──────┴──────────┘

跟着做

步骤 1:被测代码与第一个测试

$ go mod init example.com/calc

calc.go:

package calc

import "errors"

var ErrDivideByZero = errors.New("除数不能为零")

func Divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, ErrDivideByZero
	}
	return a / b, nil
}

calc_test.go(注意文件名后缀):

package calc // 与被测代码同包,可访问私有函数

import (
	"errors"
	"testing"
)

func TestDivide(t *testing.T) {
	got, err := Divide(10, 2)
	if err != nil {
		t.Fatalf("不期望的错误: %v", err)
	}
	if got != 5 {
		t.Errorf("Divide(10,2) = %v, 期望 5", got)
	}
}
$ go test ./...
ok      example.com/calc    0.3s

规则速记:文件名 *_test.go;函数名 Test 开头 + 首字母大写;参数固定 t *testing.T;t.Errorf 标记失败但继续,t.Fatalf 标记失败并终止当前测试。

步骤 2:表驱动测试 + 子测试

把多组用例排进结构体切片,一个函数测完所有场景:

package calc

import (
	"errors"
	"testing"
)

func TestDivide(t *testing.T) {
	tests := []struct {
		name    string
		a, b    float64
		want    float64
		wantErr error
	}{
		{"正常除法", 10, 2, 5, nil},
		{"负数运算", -9, 3, -3, nil},
		{"小数结果", 1, 4, 0.25, nil},
		{"除以零", 1, 0, 0, ErrDivideByZero},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) { // 每个用例一个子测试
			got, err := Divide(tt.a, tt.b)

			if tt.wantErr != nil {
				if !errors.Is(err, tt.wantErr) {
					t.Fatalf("期望错误 %v, 实际 %v", tt.wantErr, err)
				}
				return
			}
			if err != nil {
				t.Fatalf("不期望的错误: %v", err)
			}
			if got != tt.want {
				t.Errorf("Divide(%v, %v) = %v, 期望 %v", tt.a, tt.b, got, tt.want)
			}
		})
	}
}
$ go test -v ./...
=== RUN   TestDivide
=== RUN   TestDivide/正常除法
=== RUN   TestDivide/负数运算
=== RUN   TestDivide/小数结果
=== RUN   TestDivide/除以零
--- PASS: TestDivide (0.00s)
    --- PASS: TestDivide/正常除法 (0.00s)
...

只跑某个子测试:

$ go test -v -run "TestDivide/除以零" ./...

新增用例只是往表里加一行——这就是表驱动的可扩展性。

步骤 3:覆盖率与竞态

$ go test -cover ./...
ok      example.com/calc    0.3s    coverage: 100.0% of statements

$ go test -coverprofile=cover.out ./... && go tool cover -func=cover.out
example.com/calc/calc.go:7:   Divide   100.0%

$ go test -race ./...    # 竞态检测(并发代码必跑)

-race 会在测试运行时实时捕捉数据竞争并打印冲突的读写位置,是并发代码的体检仪。

步骤 4:基准测试

calc_test.go 中追加:

func BenchmarkDivide(b *testing.B) {
	b.ReportAllocs() // 输出内存分配统计
	for i := 0; i < b.N; i++ {
		Divide(10, 2)
	}
}
$ go test -bench=. -benchmem ./...
goos: windows
goarch: amd64
BenchmarkDivide-8    512345678    2.34 ns/op    0 B/op    0 allocs/op

解读:b.N 由框架自动调整(先小量跑,逐步放大直到采样稳定),保证统计可靠;2.34 ns/op 是单次操作耗时,0 allocs/op 表示零堆分配——分配数常比耗时更能暴露问题。

注意:基准里的初始化不要计入测量,用 b.ResetTimer():

func BenchmarkWithSetup(b *testing.B) {
	data := make([]int, 1000)
	for i := range data {
		data[i] = i
	}
	b.ResetTimer() // 重新计时,剔除上面的准备成本
	for i := 0; i < b.N; i++ {
		_ = data[i%1000]
	}
}

步骤 5:httptest——不启服务测 HTTP

net/http 的 handler 就是个函数,用 httptest 构造假请求和假响应即可直接调用:

// hello.go
package main

import (
	"encoding/json"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	name := r.PathValue("name")
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	json.NewEncoder(w).Encode(map[string]string{"msg": "你好, " + name})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /hello/{name}", helloHandler)
	http.ListenAndServe(":8080", mux)
}

hello_test.go:

package main

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestHelloHandler(t *testing.T) {
	tests := []struct {
		name     string
		url      string
		wantCode int
		wantBody string
	}{
		{"正常访问", "GET /hello/gopher", http.StatusOK, "gopher"},
		{"名字带编码", "GET /hello/%E4%BD%A0", http.StatusOK, "你"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "", nil)
			// 为了演示 PathValue,手动构造带 pattern 的请求:
			req.SetPathValue("name", "gopher")
			if tt.url != "GET /hello/gopher" {
				req.SetPathValue("name", "你")
			}

			rec := httptest.NewRecorder()
			helloHandler(rec, req) // 直接调用,无需端口、无需网络

			if rec.Code != tt.wantCode {
				t.Errorf("状态码 = %d, 期望 %d", rec.Code, tt.wantCode)
			}
			if !strings.Contains(rec.Body.String(), tt.wantBody) {
				t.Errorf("响应体 %q 不包含 %q", rec.Body.String(), tt.wantBody)
			}
		})
	}
}

要点:httptest.NewRequest 造请求、httptest.NewRecorder 造可检查的 ResponseWriter、直接调 handler——毫秒级完成,适合放进 CI。要测「经过路由的完整链路」,把 mux 传给 httptest.NewServer 即可。

讲清楚要点

  1. 测试文件不进产物:go build 完全忽略 _test.go;测试代码可以随意使用 testing 包而不污染业务。
  2. 同包测试(white-box)与 _test 包测试(black-box):测试文件声明 package calc 可测私有函数;声明 package calc_test 只测导出 API,更贴近使用者视角。两者可混用。
  3. 表驱动是社区标准:用例即数据,新增场景加一行;配合 t.Run 得到独立命名、可单独过滤、可并行的子测试。
  4. Errorf 与 Fatalf 的区别:前者记录失败继续执行(适合循环内继续收集信息);后者立刻终止当前测试函数(前置条件失败时)。
  5. 基准由框架自适应放大 b.N:不要自己猜次数;对比性能用 -bench 固定环境多次取均值(benchstat 工具)。
  6. 没有断言库是刻意的:if got != want { t.Errorf(...) } 就是标准做法;第三方断言库(testify)存在但非必需。

你可能会懵的地方

  • 为什么 Go 不提供 assert 断言库? 官方理由:断言宏隐藏控制流,且报错文案千篇一律。Go 用普通 if + t.Errorf 显式书写,配合 %v 打印,可读性和定制性更好。团队需要时再引 testify,但先掌握原生写法。

  • b.N 每次运行都不一样,正常吗? 正常。框架动态调整迭代次数直到耗时达到稳定采样区间(约 1 秒)。跨版本对比性能时关注 ns/op 的相对变化而非绝对值。

  • 怎么测试 goroutine / channel 并发代码? 先 -race 扫一遍;同步点用 channel/WaitGroup 显式等待,而不是 sleep;对「永远不会发生」的分支用 select { case <-time.After(...) ; t.Fatal } 防测试挂死。

  • mock 怎么做? Go 惯用「接口替身」而非 mock 框架:为依赖定义小接口(如 type Store interface{ Get(...) }),测试时传一个内存假实现。接口越小,替身越省力——这也是接口要小的另一个理由。

  • 测试函数里的循环变量捕获问题还在吗? Go 1.22 起循环变量每轮独立,老版本里 t.Run 闭包捕获 tt 的经典坑已消除;但显式用 tt := tt 或以参数传入的写法依然兼容且清晰。

错法对照

错法问题正解
测试文件名写成 calcTest.go不被工具链识别为测试必须 calc_test.go 下划线后缀
函数名 testDivide / Testdivide被忽略,no tests to runTest + 首字母大写
测试里 time.Sleep 等异步结果又慢又不稳定channel / WaitGroup 显式同步
基准里先建大数据再测,不 ResetTimer初始化开销污染结果b.ResetTimer()
用 panic 让测试失败输出是 panic 栈而非失败报告t.Fatalf("got %v want %v", ...)
断言只写 if got != want 不打印值失败时无从排查t.Errorf("... = %v, want %v")

卡点急救

  1. no tests to run:函数名不符合 TestXxx,或 -run 正则没匹配上;go test -v ./... 全量确认。
  2. panic: test timed out:测试挂死(等一个没人写的 channel、死锁)。加上超时信号与 select 保护,再 -race 排查。
  3. 覆盖率显示 0%:包路径不对;用 ./... 覆盖所有包,且测试文件与被测代码同目录。
  4. 本地过 CI 挂:常见于依赖时间、随机顺序、端口资源。测试要确定性:注入时钟/种子、httptest 分配端口。

面试常问

  • 什么是表驱动测试?为什么 Go 社区推崇? 把输入/期望值组织成结构体切片,循环 + t.Run 执行断言。优点:新增用例零成本、失败报告带用例名、逻辑与数据分离、天然适配并发(t.Parallel());标准库源码即范例。

  • 如何测试 HTTP 接口? 单 handler:httptest.NewRequest + NewRecorder 直接调用,零网络开销;整链路:httptest.NewServer(mux) 起临时服务用真 HTTP 客户端打。配合表驱动覆盖状态码与响应体。

  • 基准测试怎么读结果? ns/op 单次耗时、B/op 堆内存、allocs/op 分配次数。分配次数高往往先于耗时暴露问题(GC 压力);跨提交对比用 benchstat 做统计显著性判断,避免单次运行的噪声。

  • Go 如何做依赖注入与 mock? 面向小接口编程:消费方定义所需接口,生产实现与测试替身(fake/stub)都实现它;测试里注入假实现替代 DB/HTTP。不依赖运行时反射的 mock 框架,代码即替身。

自测三问

  1. t.Errorf 和 t.Fatalf 的区别?表驱动循环里为什么常用 Fatalf 处理前置错误?
  2. 写出表驱动测试的骨架代码(表格结构 + t.Run 循环)。
  3. BenchmarkXxx 里为什么要 b.ResetTimer()?ns/op 和 allocs/op 各说明什么?

小结一句话

Go 的测试哲学 = 工具链内置(test/cover/bench/race)+ 表驱动用例 + 小接口替身 mock;测试写得顺,是因为第 4 课起你写的每个接口都为它铺了路。

延伸

  • 官方教程:为 Go 模块添加测试
  • testing 包文档
  • 下一课:综合练习——把 12 节课的武器合起来,从零交付一个带测试的 REST API 项目。
加载本节练习…
← 上一课Web 与数据库下一课 →综合练习