推荐封装可复用的重试函数,关键参数包括最大重试次数、初始延迟和错误判定逻辑,避免简单for循环+time.Sleep导致卡死或压垮下游。

Go 中怎么写一个带重试的函数?
直接用 retry.Do 最省事,但标准库不提供,得自己实现或引入第三方。核心是控制重试次数、间隔和失败判断——别一上来就套 for 循环加 time.Sleep,容易卡死或打爆下游。
推荐封装成可复用函数,关键参数:最大重试次数、初始延迟、错误判定逻辑。例如:
func Do(fn func() error, maxRetries int, baseDelay time.Duration) error {
var err error
for i := 0; i <= maxRetries; i++ {
err = fn()
if err == nil {
return nil
}
if i == maxRetries {
break
}
time.Sleep(baseDelay * time.Duration(1<<uint(i))) // 指数退避
}
return err
}1 实现 1, 2, 4, 8 倍增长,避免浮点运算和精度问题- 第 0 次调用不 sleep,失败后才开始退避,符合直觉
- 别用
math.Pow,整数位移更快更安全
为什么指数退避比固定间隔更合理?
固定间隔(比如每次等 100ms)在服务雪崩时会加剧冲突;而指数退避让并发请求逐渐“错开”,降低重试洪峰。但退得太狠也不行——5 次重试后延迟可能到几秒,对用户响应敏感的场景不合适。
- 典型配置:
maxRetries=3,baseDelay=10ms→ 实际等待:0ms、10ms、20ms、40ms - 超时必须独立控制,不能只靠重试次数。建议外层加
context.WithTimeout - 网络类错误(如
net.OpError)适合重试;业务错误(如user_not_found)重试无意义,需提前过滤
如何避免重试逻辑污染主业务代码?
把重试抽成中间件式包装器,而不是每个 HTTP 调用都手写 for 循环。用函数选项模式(functional options)提升灵活性:
立即学习“go语言免费学习笔记(深入)”;
type RetryOption func(*retryConfig)
<p>func WithMaxRetries(n int) RetryOption {
return func(c *retryConfig) { c.maxRetries = n }
}</p><p>func WithBackoff(base time.Duration) RetryOption {
return func(c *retryConfig) { c.baseDelay = base }
}</p><p>func DoWithOpts(fn func() error, opts ...RetryOption) error { ... }- 调用时:
DoWithOpts(apiCall, WithMaxRetries(2), WithBackoff(5*time.Millisecond)) - 避免全局变量或硬编码参数,不同接口可配不同策略
- 日志建议打在每次重试前,格式如
"retrying after %v (attempt %d)",方便排查是否真在退避
实际跑起来发现重试没生效?检查这几处
常见失效原因不是逻辑写错,而是被忽略的上下文或错误类型:
- 函数里用了
return errors.New("xxx"),但调用方用errors.Is(err, xxx)判断失败——重试函数没做这个判断,直接全量重试,可能把业务错误也重试了 - HTTP 请求用了
http.DefaultClient,它默认不设超时,单次请求卡住会阻塞整个重试流程;务必设置client.Timeout - goroutine 泄漏:重试函数内部启了 goroutine 却没处理 cancel,尤其配合 context 使用时
- 时间单位写错,比如传
10当作毫秒,实际是纳秒——time.Sleep(10)几乎不暂停
重试本身不解决根本问题,只是提高容错概率;真正要盯的是失败率突增时,底层服务是否已不可用,而不是单纯调高重试次数。


















