Go循环启动协程结合CancelContext的异常问题咨询
Go CancelContext 重试场景问题解答
场景说明
处理器需要执行3次重试,每次重试耗时,需用CancelContext控制超时。
处理器函数
func process(ctx context.Context, ch chan<- int) { fmt.Println("process execute") select { case <-ctx.Done(): fmt.Println("process receive done signal, return") return default: fmt.Println("execute default") time.Sleep(4 * time.Second) // 模拟业务处理 ch <- 1 return } }
场景1:循环外定义CancelContext
代码
func main(){ ctx, cancel := context.WithCancel(context.Background()) // 全局CancelContext for i := 0; i < 3; i++ { // 重试3次 flag := make(chan int, 1) go process(ctx, flag) select { case <-flag: fmt.Println("main receive task done") case <-time.After(time.Second * 3): cancel() fmt.Println("main receive timeout ") } } }
输出
process execute execute default main receive timeout process execute process receive done signal, return main receive timeout process execute process receive done signal, return main receive timeout
问题解答:为什么execute default仅在第一次循环中打印?
第一次循环超时后调用了cancel(),导致这个全局的ctx被永久取消。后续两次循环启动的process goroutine使用的是同一个已取消的ctx,进入函数后select会直接命中<-ctx.Done()分支,跳过default,所以不会打印execute default。
场景2:循环内定义CancelContext
代码
func main(){ for i := 0; i < 3; i++ { ctx, cancel := context.WithCancel(context.Background()) // 每次循环新建CancelContext flag := make(chan int, 1) go process(ctx, flag) select { case <-flag: fmt.Println("main receive task done") case <-time.After(time.Second * 3): cancel() fmt.Println("main receive timeout ") } } }
输出
process execute execute default main receive timeout process execute execute default main receive timeout process execute execute default main receive timeout
问题解答:为何process函数从未打印process receive done signal, return?
不是没收到ctx.Done()信号,而是信号来得太晚:
- 每次循环新建的
ctx在启动process时是正常状态,此时process里的select会优先执行default分支(因为ctx.Done()通道还未就绪),随后进入time.Sleep(4*time.Second)。 - main里的超时逻辑在3秒后调用
cancel(),但此时process已经进入Sleep,之前的select已经执行完毕,不会再监听ctx.Done()的变化。 - 本质是
select在这里是一次性的非阻塞检查,不是持续监听状态变化。一旦走了default分支,后续ctx被取消也不会触发Done分支的代码。
内容的提问来源于stack exchange,提问作者ChenDehua
相关产品推荐
相关产品推荐

