gostub.Stub可安全打桩全局变量,但必须传变量地址(&var),确保可寻址、defer Reset()恢复,且推荐重构为结构体字段实现依赖注入以保障并发安全与可测性。

gostub.Stub 可以安全打桩全局变量,但必须传地址
Go 允许对包级导出或未导出的全局变量打桩,前提是用 gostub.Stub 传入变量的指针(&var),而非变量本身。直接传值会导致编译错误或桩无效。
- 变量必须是可寻址的:不能是常量、字面量、函数返回值等
- 打桩后原变量值被临时覆盖,
defer stubs.Reset()必须调用,否则污染后续测试 - 多个测试并发运行时,若共享同一全局变量,需确保每个测试独占
stubs实例,避免互相干扰 - 示例:
var size = 5 func TestSize(t *testing.T) { stubs := gostub.Stub(&size, 12) defer stubs.Reset() if got := Size(); got != 10 { t.Fatal("expected 10") } }
gomonkey.ApplyGlobalVar 也能打桩,但不推荐用于多 goroutine 场景
gomonkey.ApplyGlobalVar 接口更底层,支持任意类型全局变量(包括 struct、map、func 类型变量),但它修改的是运行时符号地址,**在 go test -p=N 并行模式下可能被其他测试覆盖**。
- 适用于单测串行执行、或变量生命周期严格隔离的场景
- 若变量是函数类型(如
var LogFunc func(string)),打桩后所有调用都会走桩函数,无法按测试实例区分行为 - 相比
gostub,它缺少自动恢复机制,忘记Reset()容易导致整个测试套件失败 - 不建议用于
os/exec.Command或http.DefaultClient这类被广泛复用的全局变量
真正解耦的写法:把全局变量转成结构体字段
依赖全局变量本身就是测试脆弱性的根源。与其打桩,不如重构为可注入依赖——这是 Go 社区推荐的惯用做法。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 将变量声明从包级移到结构体中:
type Config struct { Timeout time.Duration Logger func(string) } - 生产代码初始化时传入真实值:
cfg := Config{Timeout: 30 * time.Second, Logger: log.Printf} - 测试时直接赋值闭包:
cfg := Config{Logger: func(s string) { captured = s }} - 优势:无需第三方库、天然并发安全、类型检查严格、IDE 能跳转到定义
- 缺点:需要改业务代码结构,短期成本略高,但长期维护性显著提升
容易忽略的关键点:未导出变量也能打桩,但路径必须准确
Go 不限制打桩未导出变量(如 var internalFlag = true),但 gostub 和 gomonkey 都要求你传入其所在包的完整变量引用。如果变量在 internal/ 子包里,测试文件又不在同一包,就必须通过导出的访问函数暴露地址,或者把测试放在同包下(_test.go 文件与源码同包)。
立即学习“go语言免费学习笔记(深入)”;
- 常见错误:
gostub.Stub(&internalFlag, false)在非同包测试中会报 “cannot refer to unexported name” - 正确做法:在同包内测试,或提供一个导出的 setter:
func SetInternalFlag(v bool) { internalFlag = v },再用gomonkey.ApplyMethod打桩该函数 - 注意:未导出变量打桩后,仅影响当前测试进程,不影响其他包的观察视角——这点和 C/Python 的 patch 有本质区别

















