使用带cancel的context时Go协程未终止,卡在WaitGroup.Wait()如何解决
问题根因
- 你声明的
found是无缓冲通道,当协程找到匹配Member执行found <- member时,没有其他接收方在同步读取该通道,发送操作会永久阻塞,导致该协程永远不会执行defer waitGroup.Done(),waitGroup.Wait()就会无限挂起。 - 匿名函数内部调用
ClientMemberWithID时误用了外层循环变量client,没有使用传入的参数clientToQuery,属于Go闭包循环变量复用的经典陷阱,会导致所有协程都查询最后一个client的异常。 - 现有逻辑仅在协程启动时检查了一次
ctx.Done(),如果ClientMemberWithID执行耗时较长,即使已经触发取消,协程也不会提前终止,无法实现找到结果立刻停掉所有搜索的需求。
修复方案
- 给
found通道设置长度为1的缓冲,匹配最多只会返回1个结果的业务逻辑,避免发送方阻塞。 - 修正闭包变量引用错误,使用传入的
clientToQuery参数调用查询方法。 - 往
found写入时增加ctx.Done()的监听分支,避免极端情况多个协程同时找到结果时的阻塞问题。 - 如果
ClientMemberWithID支持传入context,优先把ctx传入实现内部提前终止,进一步提升取消响应速度。
修复后完整代码
func (group *Group) MemberWithID(ID string) (*models.Member, error) { // 给found加1个缓冲,发送方不会阻塞 found := make(chan *models.Member, 1) ctx, cancel := context.WithCancel(context.Background()) defer cancel() var waitGroup sync.WaitGroup for _, client := range group.Clients { waitGroup.Add(1) go func(clientToQuery Client) { defer waitGroup.Done() // 先检查是否已经取消,避免没必要的查询 select { case <-ctx.Done(): return default: } member, _ := clientToQuery.ClientMemberWithID(ID) if member != nil { select { case found <- member: cancel() case <-ctx.Done(): } return } }(client) } waitGroup.Wait() select { case member := <-found: return member, nil default: return nil, fmt.Errorf("no member found with given id") } }
内容的提问来源于stack exchange,提问作者aralk
相关产品推荐
相关产品推荐

