Go内部启动goroutine的函数单元测试WaitGroup空指针panic问题求解
问题根因
你遇到的panic是两个问题共同导致的:
- 空指针引用:测试中初始化
Service实例时,没有给GoRoutineWaitgroup字段赋值,该字段为nil,异步goroutine执行impl.GoRoutineWaitgroup.Add(1)时直接触发空指针panic - 异步执行时序问题:
MyFunction同步返回后测试就完成了断言,但异步启动的validateMyFunction还在后台执行,测试上下文提前销毁也会引发异常
解决方案
1. 修复业务代码隐患
当前你把WaitGroup.Add(1)放在异步goroutine内是错误写法,可能出现主goroutine已经执行到Wait、但异步goroutine还没执行Add的调度时序问题,导致Wait提前返回,调整代码如下:
func (impl *Service) MyFunction(s string) bool { a := impl.Repo.FunctionOne() b := impl.Repo.FunctionTwo() fmt.Println("Executed Function One and Function two") // Add操作移到主goroutine执行,保证调度时序正确 impl.GoRoutineWaitgroup.Add(1) go impl.validateMyFunction(a,b) return true } func (impl *Service) validateMyFunction(a string,b string) { defer helpers.PanicHandler() defer impl.GoRoutineWaitgroup.Done() fmt.Println("a and b are validated") }
同时要保证生产环境初始化Service时,必须初始化GoRoutineWaitgroup字段,避免线上出现同样的空指针问题。
2. 调整单元测试逻辑
修正测试代码中未初始化WaitGroup、未等待异步逻辑完成的问题,修改后测试代码如下:
func TestMyFunction(t *testing.T) { // 原代码变量名错误,单条case的变量名不要和测试用例切片重名 tests := []struct{ input string output bool caseName string // case是Go关键字,不能作为字段名 }{ {"a", true, "sample"} } for _, test := range tests { t.Run(test.caseName, func(t *testing.T) { mockRepo := new(mockrepo.Repo) // 补全mock方法的返回值,和原接口定义对齐 mockRepo.On("FunctionOne").Return("") mockRepo.On("FunctionTwo").Return("") // 初始化Service时同步初始化WaitGroup ms := &Service{ Repo: mockRepo, GoRoutineWaitgroup: &sync.WaitGroup{}, } op := ms.MyFunction(test.input) assert.Equal(t, test.output, op) // 等待所有异步goroutine执行完成,避免后台逻辑 panic ms.GoRoutineWaitgroup.Wait() // 可选:校验mock方法是否按照预期被调用 mockRepo.AssertExpectations(t) }) } }
内部启动goroutine的函数通用测试规范
- 初始化被测试实例时,所有异步逻辑依赖的指针类型字段(WaitGroup、日志实例、依赖接口等)必须赋值,禁止留nil值
- 测试中必须等待所有异步goroutine执行完成再结束测试用例,除了WaitGroup之外,也可以用channel传递完成信号实现同步
- 如果不需要验证goroutine调度逻辑、只需要验证业务逻辑正确性,可以给结构体加
TestMode开关,测试模式下同步执行异步逻辑,简化测试流程 - 涉及mock依赖的场景,必须等待异步逻辑执行完成再校验mock调用情况,避免调用统计遗漏导致测试误判
内容的提问来源于stack exchange,提问作者Roshan
相关产品推荐
相关产品推荐

