go test 仅识别符合硬性约束的测试:文件名以_test.go结尾且与被测代码同包,函数名以大写test开头,签名必须为func(*testing.t);表驱动测试需用t.run配合tt := tt闭包修复,禁用t.fatal避免中断执行;i/o等外部依赖须抽象为接口并mock。

go test 能直接跑起来,但不按约定写,它就当没看见——测试函数名没以 Test 开头、文件没叫 _test.go、参数不是 *testing.T,全都会被忽略。
测试文件和函数必须满足硬性命名与签名约束
Go 不会自动发现“看起来像测试”的代码,只认死规矩:
- 文件名必须以
_test.go结尾(如math_test.go),且与被测代码同包(不能放在main包里测utils包的函数) - 函数名必须以
Test开头,且**首字母大写**(testAdd或TESTAdd都无效) - 函数签名必须严格是
func(t *testing.T)—— 多一个参数、少一个星号、换用*testing.B,go test就跳过它 - 跨目录运行需显式指定路径:
go test ./service,否则默认只扫当前目录下匹配主模块名的测试
用 t.Run 写表驱动测试,别堆 if 块
多个输入组合共用一套逻辑时,硬写 TestAdd1/TestAdd2 很难维护。用结构体切片 + t.Run 是标准解法:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -1, -2},
}
for _, tt := range tests {
tt := tt // 必须这行!防止闭包捕获循环变量
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.expected {
t.Errorf("Add(%d,%d) = %d, want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
漏掉 tt := tt 这句,所有子测试看到的都是最后一个 tt 值;t.Run 还能单独重跑某个 case(go test -run="TestAdd/positive"),失败定位更准。
别在循环里用 t.Fatal,改用 t.Errorf 或子测试
t.Fatal 一调就退出当前测试函数,导致后续用例根本没机会执行。常见于错误地把表驱动写成这样:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
for _, tt := range tests {
if got := Add(tt.a, tt.b); got != tt.expected {
t.Fatal("boom") // ❌ 后续 tt 全跳过
}
}
正确做法只有两个:
- 用
t.Errorf记录错误但继续跑完全部用例(适合并列断言,比如检查返回结构体多个字段) - 把每个用例拆成独立
t.Run子测试(推荐,失败隔离、可单独重试、支持t.Parallel())
另外,t.Parallel() 要放在子测试函数第一行,且一旦加了它,就不能再调用 t.Skip 或 t.Fatal(会 panic),只能用 t.Log/t.Error 系列。
真实 I/O、网络、时间依赖必须抽象成接口再 mock
测试里调 os.ReadFile、http.Get、time.Now() 看似简单,实则埋雷:
- CI 环境可能没权限读写文件,或 Redis 连不上
- 两次测试间文件残留导致结果不一致
- 时间相关逻辑(如过期判断)在秒级精度下难以稳定断言
可靠做法是提前定义接口,把依赖“注入”进来:
type Clock interface {
Now() time.Time
}
func IsExpired(t time.Time, clock Clock) bool {
return clock.Now().After(t.Add(24 * time.Hour))
}
测试时传入一个固定时间的 mock 实现,而不是动真格调系统时钟。同理,文件操作抽成 FileReader,HTTP 客户端抽成 HTTPDoer——不 mock 具体实现,只 mock 依赖契约。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










