核心是用[]struct{}定义测试数据并用t.run()包裹每个case;结构体须显式含name、input、want、wanterr bool等导出字段,禁用map或匿名struct,name需非空且语义明确,error/float64等类型需专用比较方式。

Go 里写表驱动测试,核心就一条:用 []struct{} 定义测试数据,每个 case 必须用 t.Run() 包裹,否则不是表驱动,只是带循环的普通测试。
怎么定义测试结构体才不会漏字段、不 panic
结构体必须显式包含 name、输入字段(如 input 或 url)、预期输出字段(如 want 或 wantStatusCode),以及 wantErr bool(别用 wantError error 字段存具体 error 实例)。
- 字段名要跟被测函数参数/返回值语义对齐,比如函数是
func ParseInt(s string) (int, error),结构体就该有input string、want int、wantErr bool - 避免用
map[string]interface{}或匿名 struct —— IDE 不提示、go vet检不出拼写错误、失败时打印不出字段名 - 所有字段尽量导出(首字母大写),否则某些工具(如 JSON 序列化调试)会读不到
- 如果某个 case 的输入是
nil,而被测函数没处理,就会 panic;建议在循环内加兜底:if tc.input == nil { tc.input = []string{} }
t.Run() 为什么不能省,且必须传非空 name
漏掉 t.Run(),一个 t.Fatal() 就让整个测试函数退出,后续所有 case 根本不跑;传空字符串做 name,会导致 go test -run=TestXxx/xxx 过滤失效,也看不出失败的是哪个场景。
-
name最好含关键输入摘要,比如fmt.Sprintf("input_%s", tc.input),而不是"case1" - 子测试默认并发执行,
t.Parallel()必须放在t.Run()的闭包里,放外面无效 - 所有 setup/teardown(如
t.TempDir()、mock 初始化)都得放在t.Run()函数体内,不能提前提取到循环外——否则多个子测试共享状态,一并发就错
如何安全比对 error、float64、time.Time 等易翻车类型
直接用 == 比 error 或 reflect.DeepEqual 比浮点数,是表驱动测试里最常触发误报的地方。
- error 判断只关心有无:用
if (err != nil) != tt.wantErr;要验证类型:用errors.Is(err, io.EOF);要验证自定义字段:先errors.As(err, &myErr),再断言myErr.Code - float64 比较别用
==,改用math.Abs(got - tt.want) (结构体里加 <code>tolerance float64字段) - time.Time 比较用
.Equal()方法,DeepEqual会因单调时钟或时区差异失败 - 结构体含函数、
sync.Mutex、unsafe.Pointer时,禁用reflect.DeepEqual,手动比关键字段
多返回值函数的表结构怎么设计
函数返回 (int, error) 或 (string, bool, error) 时,结构体字段必须一一对应,不能合并成一个 want interface{} 或用字符串拼接模拟。
- 例如
func half(n int) (int, bool),结构体字段就得是in int、outint int、outeven bool,名字要体现语义,不是out1/out2 - 如果两个返回值类型相同(如
(min int, max int)),字段名更要区分逻辑含义,否则后期维护极易混淆 - 不要在 table 数据里调用函数(如
time.Now()),时间相关 case 用固定time.Date(2000, 1, 1, ...) - 所有字段必须显式初始化,哪怕用零值也要写出来,比如
opts: []FormatOption{},别依赖字段省略
最常被忽略的一点:表驱动本身不防御运行时 panic。如果某个 input 是 nil,而被测函数没判空,测试就直接崩在 t.Run 外层,堆栈还指向循环变量声明处——这时候你得回看 table 数据本身是否做了基础校验,而不是怪框架或 t.Run。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











