go语言单元测试依赖隔离需从代码组织入手:接口由使用方定义并置于内部目录,i/o须作为参数传入,禁用全局变量与硬编码,优先手写轻量mock,构造函数显式接收依赖。

Go语言单元测试里,依赖隔离不是靠加个框架就自动解决的——它本质是代码组织方式的选择。你写下的每一行结构体字段、每一个函数参数、每一次 new 调用,都在决定测试是否可控。
接口必须由使用方定义,不能复用实现方的包
常见错误是直接 import 第三方服务包(比如 github.com/xxx/userpkg),然后把它的结构体当依赖注入。这会导致:测试时不得不 mock 整个包、字段变更引发 panic、无法只测“获取邮箱”这个动作。
- 订单模块只需要
GetEmailByID(ctx, id),就只定义UserEmailGetter接口,放在order/internal/dep/下 - HTTP 客户端、内存 mock、数据库查询器,只要满足该接口,就能无缝替换
- 编译期就能检查实现是否完整,而不是等
mockgen生成后才发现少方法
io.Reader/io.Writer 必须作为参数传入,不能硬编码 os.Stdin/os.Stdout
函数里出现 bufio.NewReader(os.Stdin) 或 fmt.Println,基本等于放弃单元测试。重定向 stdin/stdout 不是“技巧”,而是补救措施;真正可测的写法从第一行就拒绝全局 I/O。
-
AskQuestion(reader io.Reader, writer io.Writer, question string) string—— 输入输出都可控 - 测试时用
strings.NewReader("ABCDE")和bytes.NewBuffer(nil)即可断言输出内容 - 避免在函数内部调用
os.Exit、log.Fatal等终止行为,改用返回 error
Mock 不是越多越好,优先用轻量 hand-written mock
gomock 或 testify/mock 自动生成的 mock 代码,对简单接口反而增加维护成本。尤其当接口只有 1–2 个方法时,手写 mock 更快、更清晰、无额外依赖。
- 例如
UserRepository只有GetUser(id int) (*User, error),直接写type MockUserRepo struct { users map[int]*User } - 不用
mockgen就能支持表驱动测试中的不同返回场景(如 nil user、error、特定字段) - 复杂行为(如调用次数校验、动态返回值)才考虑引入 gomock,否则先写死再重构
构造函数必须显式接收依赖,禁止包级全局变量或 init 初始化
像 var db *sql.DB = sql.Open(...) 这种写法,会让所有测试共享状态,且无法替换为 sqlmock 实例。
- 业务结构体的构造函数应接收所有外部依赖:
func NewUserService(repo UserRepository, sender EmailSender) *UserService - 测试中可自由组合:
NewUserService(&MockUserRepo{}, &MockEmailSender{}) - 避免
init()函数里初始化任何有副作用的对象(HTTP client、logger、DB)
最难的不是写 mock,而是说服自己:那个“临时 hardcode 的 os.Stdin”、“顺手 new 的 http.Client”、“图省事 import 的整个 service 包”,全都是测试障碍的起点。解耦不是为了好看,是让 go test 运行时,每一条路径都可预测、可打断、可验证。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











