t.run嵌套不执行是因为外层函数提前返回导致内层未注册;应顶层直接调用,用斜杠命名模拟目录结构;子测试间需独立setup/teardown;日志需手动加t.name()或用-v查看完整路径。

为什么 t.Run 里再套 t.Run 会“不报错但没运行”
Go 的 t.Run 是惰性启动的:外层测试函数返回后,所有注册的子测试才真正排队执行。如果在子测试内部又调用 t.Run,但外层子测试函数已提前 return(比如因 panic、return 或 t.Fatal),内层子测试根本不会被注册到执行队列里——不是跳过,是压根没看见。
- 常见错误现象:
t.Run("outer", func(t *testing.T) { t.Run("inner", ...) })中,outer函数里写了return或t.Fatal后再调用t.Run,inner永远不出现 - 正确做法:所有
t.Run必须在当前测试函数作用域顶层直接调用,不能包裹在条件分支或 defer 之后,更不能藏在 return 语句后面 - 一个可靠写法:把嵌套逻辑拆成独立子测试,靠命名体现层级,例如
"JSON/valid"和"JSON/invalid/empty"
如何用子测试名模拟“目录结构”来组织用例
Go 测试不支持物理嵌套目录,但子测试名支持斜杠 /,go test -run 能按前缀匹配,实际效果等价于分组。
- 使用场景:想快速跑某类用例(如只测所有数据库相关),或 CI 中按模块隔离失败
- 命名建议:用
/分隔语义层级,如"Validator/Email/format"、"Validator/Email/length"、"Validator/Phone/country_code" - 运行命令:
go test -run=Validator/Email会同时匹配前两个;go test -run=^Validator/Email$(注意锚点)才能精确匹配整个名字 - 注意:斜杠只是命名约定,Go 不校验路径合法性,
"a//b"或"../x"都能注册成功,但会导致 -run 匹配混乱
子测试间共享 setup/teardown 的安全方式
子测试默认并发执行,不能靠闭包变量共享状态;每个 t.Run 函数都是独立生命周期,t.Cleanup 也只对当前子测试生效。
- 错误做法:在父测试函数里声明
db *sql.DB,然后在多个t.Run里直接复用——可能引发并发读写 panic - 推荐做法:把资源创建和清理逻辑封装进辅助函数,在每个子测试里调用,例如:
func setupDB(t *testing.T) *sql.DB { db, err := sql.Open("sqlite3", ":memory:") if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) return db } - 性能提示:如果 setup 成本高(如启 HTTP server),可考虑用 sync.Once + 全局变量缓存,但必须加锁控制初始化,并确保 cleanup 可重复执行
子测试失败时,日志里看不到父测试名怎么办
默认情况下,t.Log 输出不自动带上子测试路径,当多个同名子测试(比如都叫 "case1")失败时,很难定位是哪个分支出的问题。
立即学习“go语言免费学习笔记(深入)”;
- 根本原因:Go 测试日志系统只记录当前
*testing.T实例的 name,但输出时不拼接祖先链 - 简单补救:在每个子测试开头加
t.Log("running:", t.Name()),或者统一用封装函数:func logStart(t *testing.T) { t.Log("→", t.Name()) } - 更彻底方案:用
-v参数运行,Go 会输出完整子测试名(含斜杠路径)+ 日志行,例如=== RUN TestParse/JSON/valid,这时配合t.Log就能对齐










