Go语言无内置自动重试机制,需手动实现可取消、可超时、可重试判断的重试逻辑,并配合限流、指数退避与错误分类策略。

Go 语言本身不提供内置的“自动重试”原语,go 关键字只启动协程,select 和 channel 负责协调,重试逻辑必须手动编码实现;盲目并发重试反而容易触发雪崩或被限流。
用 time.After + select 实现单次请求带超时的重试
这是最常见也最容易出错的模式:不是简单套个 for 循环加 time.Sleep,而是要让每次重试都可取消、可超时。
- 每次重试前新建一个
context.WithTimeout,避免上一次超时影响下一次 - 用
select同时监听ctx.Done()和业务结果ch,防止 goroutine 泄漏 - 错误判断不能只看
err != nil,要区分是否可重试(比如net.OpError可重试,json.SyntaxError不该重试)
示例关键片段:
for i := 0; i < maxRetries; i++ {
ctx, cancel := context.WithTimeout(parentCtx, perRetryTimeout)
defer cancel() // 注意:这里 defer 放错位置会导致只取消最后一次
<pre class="brush:php;toolbar:false;">ch := make(chan result, 1)
go func() {
res, err := doRequest(ctx)
ch <- result{res, err}
}()
select {
case r := <-ch:
if r.err == nil {
return r.res, nil
}
if isRetryable(r.err) {
continue
}
return nil, r.err
case <-ctx.Done():
if i == maxRetries-1 {
return nil, ctx.Err()
}
// 继续下一次重试
}}
立即学习“go语言免费学习笔记(深入)”;
用 sync.WaitGroup 控制并发重试的总数量
当需要对一批任务(如 100 个 API 调用)统一做失败重试时,直接起 100 个 goroutine 并发重试可能打爆下游。必须限制并发度。
-
sync.WaitGroup本身不控制并发,它只是等待;真正限流靠semaphore或带缓冲的 channel - 推荐用
make(chan struct{}, maxConcurrency)当信号量:每次重试前,结束后 <code>sem - 重试任务应封装为独立函数,接收原始参数和重试次数,避免闭包捕获循环变量问题(常见 bug:
for _, item := range items { go f(item) }中所有 goroutine 共享最后一个item)
用 backoff.Retry 避免轮询式重试压垮服务
第三方库 github.com/cenkalti/backoff/v4 提供了指数退避支持,比手写 time.Sleep(time.Second * time.Duration(1 更可靠。
- 不要直接传
backoff.NewExponentialBackOff(),它默认最大重试时间是 30 分钟,可能远超你的 SLA - 务必调用
b.Reset(),否则多次重试会复用同一个已过期的 backoff 实例 - 配合
backoff.WithContext使用,确保上下文取消能中断退避等待 - 注意:该库的
Retry函数是同步阻塞的,若需并发执行多个重试任务,仍需外层加 goroutine 和限流
重试逻辑必须和错误分类强绑定
同一段重试代码,在不同错误类型下行为差异极大——这也是最容易被忽略的设计点。
-
context.DeadlineExceeded:说明上游已放弃,不应再重试 -
*url.Error且Err是net.OpError:网络层错误,通常可重试 -
io.EOF或http.StatusTooManyRequests:前者不可重试,后者需检查响应头Retry-After并动态调整间隔 - 自定义错误(如
ErrRateLimited)建议实现Temporary() bool方法,方便统一判断
真正难的不是写重试代码,而是定义清楚:哪些错误算“临时性”,重试几次后必须放弃,以及每次间隔是否该随错误类型变化。这些决策一旦写死在工具函数里,后续很难调整。










