go测试文件必须与生产代码同包才能访问未导出标识符;测试文件名须以_test.go结尾且包声明一致;应按业务模块而非测试类型组织测试子包;避免全局状态污染,推荐函数级setup/cleanup;用-go:build标签和-run参数精准控制测试执行。

测试文件和生产代码必须放在同一包里
Go 的测试文件(*_test.go)如果想直接访问未导出的函数、字段或变量,就必须和被测代码在同一个包下。这是 Go 测试机制的硬性要求,不是约定——编译器会直接报错 cannot refer to unexported name xxx。
常见错误是新建一个 test/ 目录,把所有测试挪进去并改包名为 test,结果发现根本测不了内部逻辑。别这么干。
- 测试文件名必须以
_test.go结尾,且包声明和源码包名一致(比如package service) - 如果确实需要跨包测试(比如集成测试),只能通过导出接口 + 公共构造函数暴露能力,不能靠“绕过封装”来测
- 同包测试不等于同目录:你可以把
user_service_test.go放在service/下,也可以放在service/internal/里,只要包名对就行
按功能边界切分测试子包,而不是按测试类型
别建 unit/、integration/、e2e/ 这类顶层测试目录。Go 没有测试运行时的“测试类型”识别机制,go test 只看包路径和文件名。强行分层只会让 import 路径变长、依赖关系混乱、IDE 跳转失灵。
真正有效的分法是跟着业务模块走:比如 auth/ 包下放 auth_test.go 和 auth_integration_test.go;payment/ 包里放自己的测试,各自独立运行、各自管理依赖。
立即学习“go语言免费学习笔记(深入)”;
- 每个业务子包自包含测试,用
go test ./auth就能跑完 auth 全部逻辑,不牵扯其他模块 - 集成测试文件名用
_integration_test.go后缀,配合-short或自定义build tag控制是否执行(比如加//go:build integration) - 避免测试包之间相互 import:测试代码不是库,不需要复用,复用只会增加耦合和假阳性
慎用 init() 和全局状态在测试中
很多老项目在测试前用 init() 注册 mock、重置全局 config、初始化数据库连接池——这会导致测试间污染。一次 go test -race 失败往往就卡在这儿。
Go 测试默认并发执行(-p 默认为 CPU 核数),init() 只跑一次,但多个测试函数共享同一份全局状态,A 测试改了某变量,B 测试就拿到脏数据。
- 把 setup/cleanup 逻辑写进每个测试函数开头/结尾,或者用
TestMain统一做一次进程级初始化(仅限 truly global 且不可变的东西) - 数据库连接、HTTP client、缓存实例等,应该在每个测试函数里 new 出来,或通过参数传入(推荐用 interface + 构造函数)
- 如果非要用全局变量(比如 logger 实例),确保它是线程安全的,或用
sync.Once+ 显式 reset 方法控制生命周期
go test 的 -tags 和 -run 是分场景执行的关键
大型项目不可能每次改一行代码都跑全量测试。靠目录结构区分不够灵活,得靠命令行参数精准控制。
-run 匹配测试函数名(支持正则),-tags 控制 build tag,两者组合能实现「只跑某个模块的集成测试」或「跳过耗时的第三方依赖测试」。
- 给慢速测试加
//go:build !unit,单元测试时用go test -tags=unit快速验证逻辑 - 用
go test -run=^TestUserLogin$ ./auth精确调试单个 case,避免误触发整个 suite - CI 中按需组合:单元测试走
-tags=unit,集成测试走-tags=integration -timeout=60s
最常被忽略的是:不同测试文件里同名函数(比如都叫 TestMain)会导致 go test 报错 multiple TestMain functions。这不是 bug,是设计——每个包最多一个 func TestMain,别复制粘贴测试骨架。










