Go 表驱动测试需用显式命名结构体、t.Run 分组、循环中复制变量,避免 map/平行切片;error 用 errors.Is/As,float 用 math.Abs 比较。

Go 语言没有参数化测试语法,表驱动测试(table-driven test)不是“可选技巧”,而是唯一被标准库和主流项目验证的多用例组织方式。不这么写,测试就难定位、易漏跑、一改全挂。
怎么定义测试结构体才不会在加字段时崩掉
结构体字段必须显式命名、语义对齐、零值可控。别用 map[string]interface{} 或平行切片(如 inputs []string 和 expects []int),否则增删字段或用例时极易错位,IDE 无法跳转,go vet 也检查不到字段缺失。
- 输入字段名直接对应函数参数:被测函数是
ParseHeader(s string),就用input string,不用in或val - 预期字段按返回值拆开:若函数返回
(int, error),结构体里就要有wantN int和wantErr bool(或wantErr error),别合并成want interface{} - 所有字段尽量导出(首字母大写),否则未来工具(如自动生成测试报告)可能读不到
- 初始化时显式写出字段名:
{name: "empty_input", input: "", wantN: 0, wantErr: true},不依赖顺序匹配
t.Run() 为什么不能省,以及名字怎么起才不白费
漏掉 t.Run(),一个 t.Fatal() 就终止整个测试函数,后续用例全不执行;失败日志只显示 TestParseHeader,看不出是哪组数据挂了;go test -run=TestParseHeader/empty_input 这类子测试过滤也完全失效。
-
name字段必须非空,且含语义:用"empty_input_returns_error",别用"case1"或"test_0" - 名字里含
/会自动分组,比如"Parse/valid"和"Parse/invalid_format"在go test -v输出中缩进显示,方便归类 - 禁止拼接变量生成名字:
"TestParse_" + tc.input可能因空格或特殊字符 panic,也导致日志混乱 - 名字长度控制在 64 字符内,过长会影响
go test -v对齐和 CI 日志可读性
for 循环里直接用 tt 为什么会全跑错
这是 Go 新手最高频的坑:循环变量 tt 是复用的栈地址,闭包捕获的是指针,不是值。所有 t.Run() 实际访问的都是最后一组数据——现象是只有最后一个用例通过,其余全失败,或所有失败信息一模一样。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { use tt.input }) } - 正确写法一(推荐):
for _, tt := range tests { tt := tt; t.Run(tt.name, func(t *testing.T) { use tt.input }) } - 正确写法二:
for i := range tests { tt := tests[i]; t.Run(tt.name, func(t *testing.T) { use tt.input }) } - 如果结构体较大,复制开销敏感,可改用索引访问,但需确保
tests[i]不为 nil
error、float、time 这些类型怎么比才不翻车
直接用 == 比 error 或 DeepEqual 比浮点数、time.Time,大概率因底层实现细节或精度问题误判。
- 判断是否出错:用
if (err != nil) != tt.wantErr,而不是err == nil - 验证具体 error 类型:用
errors.Is(err, io.EOF)或errors.As(err, &target),别比err.Error()字符串 - 浮点比较:用
math.Abs(got - tt.wantFloat) ,别存 <code>want float64然后DeepEqual -
time.Time比较:用got.Equal(tt.wantTime),DeepEqual受单调时钟、时区字段影响不可靠 - 复杂 struct 比较:若只关心几个字段,手动断言比全量
DeepEqual更清晰、更易调试
最常被忽略的其实是「每个子测试必须自包含」:临时目录、HTTP mock、文件句柄,都得在 t.Run() 函数体内初始化并用 t.Cleanup() 清理。一旦塞到循环外,case 之间就会状态污染,t.Parallel() 彻底失效,CI 上偶发失败也查无踪迹。


















