Go中无自动重试,需显式实现循环+错误判断+延迟;难点在于判断何时不该重试及避免压垮系统,应使用backoff/v4等库实现带抖动的指数退避并严格分类错误。

直接上结论:Go 里没有自动重试,所谓“自动”全是靠你显式控制循环 + 错误判断 + 延迟等待实现的;重试本身不难,难在「什么时候不该重试」和「重试时别把系统拖垮」。
为什么不能直接用 for + time.Sleep 硬写重试?
看似最简单的写法,实际埋了三个雷:
- 忽略
ctx.Done():哪怕请求已被取消,time.Sleep还在傻等,goroutine 卡住不退出 - 固定间隔压垮下游:100 个协程同时重试,全在第 2 秒准时发起第二次请求,就是重试风暴
- 错误无分类:把
sql.ErrNoRows或errors.New("invalid token")和net.OpError一样重试,纯属浪费资源
所以别从零手撸,优先用成熟库封装好的退避逻辑。
用 backoff.Retry 实现带抖动的指数退避
社区最稳的选择是 github.com/cenkalti/backoff/v4,它把 jitter、context 取消、最大耗时都收口了,你只管定义「什么算失败」:
立即学习“go语言免费学习笔记(深入)”;
- 必须用
backoff.WithContext(bo, ctx)包一层,否则ctx.Done()完全无效 - 必须加
backoff.WithJitter(),否则多个实例会在同一时刻重试 -
InitialInterval建议设为100 * time.Millisecond,MaxInterval别超2 * time.Second,避免单次等待过长 - 操作函数内部仍要检查
ctx.Err(),比如 HTTP 请求里传入ctx给http.NewRequestWithContext
示例片段:
<pre class="brush:php;toolbar:false;">bo := &backoff.ExponentialBackOff{
InitialInterval: 100 * time.Millisecond,
MaxInterval: 2 * time.Second,
MaxElapsedTime: 10 * time.Second,
Clock: backoff.SystemClock,
}
bo = backoff.WithJitter(bo)
<p>err := backoff.Retry(func() error {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err // 网络层错误,可重试
}
defer resp.Body.Close()
if resp.StatusCode == 503 || resp.StatusCode == 429 {
return fmt.Errorf("temp error: %d", resp.StatusCode) // 临时服务错误,重试
}
if resp.StatusCode >= 400 {
return backoff.Permanent(fmt.Errorf("client error: %d", resp.StatusCode)) // 永久错误,终止重试
}
return nil
}, backoff.WithContext(bo, ctx))</p>如何记录每次重试的错误而不干扰主逻辑?
错误日志不是可选功能,而是调试重试行为的唯一依据。关键点:
- 别在重试函数里直接
log.Printf——它会污染返回值,且无法区分是第几次失败 - 用
backoff.Notify回调,在每次重试前/后拿到错误和当前尝试次数 - 记录内容至少包含:
attempt、error、nextDelay(下次等待多久),方便排查是否卡在某类错误上 - 对高频重试操作,考虑加采样(如每 10 次记 1 条),避免日志刷爆磁盘
示例:
<pre class="brush:php;toolbar:false;">var attempt int
err := backoff.RetryNotify(func() error {
attempt++
return doSomething()
}, backoff.WithContext(bo, ctx), func(err error, t time.Duration) {
log.Printf("retry[%d] failed: %v, next in %v", attempt, err, t)
})
哪些错误绝对不能重试?
这是最容易被跳过的环节,但错判会导致无限循环或业务异常:
4xx HTTP 状态码(除 <code>429 Too Many Requests):参数错、权限不足、资源不存在,重试无意义-
sql.ErrNoRows、fmt.Errorf("user not found"):业务逻辑明确失败,不是临时问题 -
context.Canceled、context.DeadlineExceeded:调用方已放弃,你还重试就是资源泄漏 -
io.EOF、json.SyntaxError:数据解析失败,不是连接问题,重试只会再错一遍
真正值得重试的,基本就三类:net.OpError(连接超时、拒绝)、503 Service Unavailable、自定义的临时错误如 ErrRateLimited。其他一律用 backoff.Permanent() 终止。
最后提醒一句:重试函数本身必须幂等。如果 fn() 里有写数据库、发消息这类副作用,重试就会重复执行——这不是容错,是制造数据错误。


















