不会卡死整个程序,但会阻塞当前P导致该goroutine无法被调度;正确做法是每次重试后用time.Sleep让出时间片,并通过select+ctx.Done()响应取消以避免泄漏,推荐指数退避。

goroutine里直接用for循环重试会卡死吗
会。如果重试逻辑没加延迟或退出条件,for 会立刻反复执行,吃满一个P,且无法被调度器中断——这不是“并发重试”,是单线程死循环。
正确做法是每次失败后用 time.Sleep 让出时间片,并配合上下文控制生命周期:
- 必须用
select+ctx.Done()响应取消,否则 goroutine 可能泄漏 - 重试间隔建议用指数退避(如
time.Second * time.Duration(1),避免雪崩 - 别在重试循环里直接调用
time.Sleep后续无select,否则 ctx 超时也无法退出
用context.WithTimeout包装重试任务是否足够
不够。仅靠 context.WithTimeout 只能限制整个重试流程的总耗时,但无法控制单次请求超时或重试次数上限。
典型陷阱是:网络请求本身没设超时,一次卡住 30 秒,总共只允许 5 秒,结果第一次就超时失败,根本没机会重试。
立即学习“go语言免费学习笔记(深入)”;
- 对外部调用(如 HTTP、DB)必须单独设超时,例如
http.Client.Timeout或context.WithTimeout(ctx, 2*time.Second)传给Do - 重试次数建议显式计数,而不是依赖总时间倒推——网络抖动时,短超时+多次重试比长超时+少次更可靠
-
context.WithTimeout的ctx应该作为参数传入重试函数,不能在内部重新创建
封装一个可复用的retry.Do函数要注意什么
Go 标准库没有内置重试,自己封装时最容易漏掉错误分类处理——比如网络临时错误(net.OpError)值得重试,而 JSON 解析失败(json.SyntaxError)重试也没用。
- 接收一个
func() error执行体,和一个func(error) bool判定函数,决定是否重试该错误 - 不要硬编码退避策略,把间隔计算逻辑抽成
func(attempt int) time.Duration - 返回值必须包含最后一次的
error,否则调用方无法区分“重试全失败”和“成功” - 示例关键片段:
for attempt := 0; attempt < maxRetries; attempt++ { err := operation() if err == nil { return nil } if !shouldRetry(err) { return err } select { case <-time.After(backoff(attempt)): case <-ctx.Done(): return ctx.Err() } }
多个goroutine并发重试共享同一个context会互相影响吗
会,但这是预期行为。所有 goroutine 共享同一个 ctx 时,只要任意一个触发 cancel(),全部都会收到 ctx.Done() 并退出。
这在批量任务中很常见,比如发 10 个 API 请求,任一请求超时或手动取消,其余也该停止重试。
- 如果需要独立生命周期,每个 goroutine 必须用
context.WithCancel(parentCtx)派生自己的子 ctx - 注意:派生子 ctx 后,要记得调用
cancel(),否则可能泄漏 goroutine(尤其在提前成功时) - 共享 ctx 时,别在重试函数里调用
cancel()—— 这会误杀其他协程
重试逻辑里最常被忽略的是错误语义判断:不是所有 error 都该重试,也不是所有重试都该等同样久。退避策略、错误过滤、上下文传播,三者缺一不可。


















