在Golang的实际开发中,很多操作会因为网络波动、服务临时不可用等原因出现临时性错误,这类错误不需要立即判定为业务失败,通过重试往往可以恢复正常执行。错误重试机制的核心是在操作失败时,按照预设的策略重复执行操作,直到成功或者达到最大重试次数。

错误重试的核心设计要点
实现错误重试机制前,需要先明确几个核心要素:
- 重试触发条件:需要明确哪些错误需要触发重试,比如网络超时错误可以重试,参数校验错误则不需要重试
- 最大重试次数:避免无限重试导致程序资源耗尽,需要设置合理的最大重试上限
- 重试间隔策略:常见的有固定间隔、指数退避两种,指数退避可以减少对下游服务的压力
- 超时控制:结合context控制整个重试过程的总超时时间,避免长时间阻塞
固定间隔重试实现
固定间隔重试是指每次重试之间等待固定的时间,实现逻辑比较简单,适合对重试间隔要求不高的场景。
首先定义重试函数的通用签名,支持传入执行函数、最大重试次数、重试间隔:
package main
import (
"errors"
"fmt"
"time"
)
// 定义执行函数的类型,返回错误
type RetryFunc func() error
// 固定间隔重试函数
// maxRetry: 最大重试次数
// interval: 每次重试间隔
// fn: 需要执行的函数
func FixedIntervalRetry(maxRetry int, interval time.Duration, fn RetryFunc) error {
var err error
// 先执行一次原始调用
err = fn()
if err == nil {
return nil
}
// 如果失败,开始重试
for i := 1; i <= maxRetry; i++ {
// 等待重试间隔
time.Sleep(interval)
err = fn()
if err == nil {
return nil
}
fmt.Printf("第%d次重试失败,错误: %vn", i, err)
}
// 所有重试都失败,返回最后一次的错误
return fmt.Errorf("达到最大重试次数%d次,最后错误: %v", maxRetry, err)
}
// 模拟一个可能失败的操作
func mockOperation() error {
// 模拟随机失败,前两次执行返回错误,第三次成功
staticCount := 0
return func() error {
staticCount++
if staticCount < 3 {
return errors.New("模拟临时网络错误")
}
return nil
}()
}
上面的FixedIntervalRetry函数实现了固定间隔重试逻辑,调用方式如下:
func main() {
// 设置最大重试3次,每次间隔1秒
err := FixedIntervalRetry(3, time.Second, mockOperation)
if err != nil {
fmt.Printf("操作最终失败: %vn", err)
} else {
fmt.Println("操作执行成功")
}
}
指数退避重试实现
固定间隔重试可能会对下游服务造成集中压力,指数退避重试的间隔会逐次翻倍,比如第一次间隔1秒,第二次2秒,第三次4秒,可以有效降低重试对服务的影响。
实现指数退避重试只需要调整等待间隔的计算逻辑:
package main
import (
"errors"
"fmt"
"math"
"time"
)
type RetryFunc func() error
// 指数退避重试函数
// maxRetry: 最大重试次数
// baseInterval: 基础间隔,第一次重试的间隔
// fn: 需要执行的函数
func ExponentialBackoffRetry(maxRetry int, baseInterval time.Duration, fn RetryFunc) error {
var err error
err = fn()
if err == nil {
return nil
}
for i := 1; i <= maxRetry; i++ {
// 计算指数退避间隔:baseInterval * 2^(i-1)
interval := time.Duration(math.Pow(2, float64(i-1))) * baseInterval
fmt.Printf("第%d次重试,等待间隔: %vn", i, interval)
time.Sleep(interval)
err = fn()
if err == nil {
return nil
}
fmt.Printf("第%d次重试失败,错误: %vn", i, err)
}
return fmt.Errorf("达到最大重试次数%d次,最后错误: %v", maxRetry, err)
}
结合Context控制重试超时
前面的两种重试实现都没有考虑总超时的问题,如果单次操作耗时较长,重试多次可能会阻塞很久,结合context可以设置整个重试过程的最大超时时间,超时后直接终止重试。
package main
import (
"context"
"errors"
"fmt"
"time"
)
type RetryFunc func() error
// 带超时的重试函数,支持固定间隔和指数退避两种模式
// ctx: 上下文,用于控制超时
// maxRetry: 最大重试次数
// interval: 固定间隔模式下使用,指数退避模式下作为基础间隔
// isExponential: 是否使用指数退避
// fn: 执行函数
func RetryWithContext(ctx context.Context, maxRetry int, interval time.Duration, isExponential bool, fn RetryFunc) error {
var err error
// 先执行一次原始调用
err = fn()
if err == nil {
return nil
}
for i := 1; i <= maxRetry; i++ {
// 检查上下文是否已经超时或者取消
select {
case <-ctx.Done():
return fmt.Errorf("重试过程被取消: %v,最后错误: %v", ctx.Err(), err)
default:
}
// 计算重试间隔
var currentInterval time.Duration
if isExponential {
currentInterval = time.Duration(1<<(i-1)) * interval // 用位运算替代math.Pow,更高效
} else {
currentInterval = interval
}
// 等待重试间隔,同时监听上下文状态
timer := time.NewTimer(currentInterval)
select {
case <-ctx.Done():
timer.Stop()
return fmt.Errorf("重试等待时被取消: %v,最后错误: %v", ctx.Err(), err)
case <-timer.C:
}
err = fn()
if err == nil {
return nil
}
fmt.Printf("第%d次重试失败,错误: %vn", i, err)
}
return fmt.Errorf("达到最大重试次数%d次,最后错误: %v", maxRetry, err)
}
// 模拟可能超时失败的操作
func mockTimeoutOperation() error {
staticCount := 0
return func() error {
staticCount++
if staticCount < 3 {
// 模拟操作耗时2秒
time.Sleep(2 * time.Second)
return errors.New("模拟操作超时")
}
return nil
}()
}
调用带超时的重试函数示例:
func main() {
// 设置总超时时间为3秒,最多重试2次,使用固定间隔1秒
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
err := RetryWithContext(ctx, 2, time.Second, false, mockTimeoutOperation)
if err != nil {
fmt.Printf("操作失败: %vn", err)
} else {
fmt.Println("操作执行成功")
}
}
注意事项
在实现错误重试机制时,还需要注意几个问题:
- 不是所有错误都适合重试,比如参数错误、权限错误等永久性错误,重试没有意义,需要在执行函数中过滤这类错误,不返回可重试的错误类型
- 重试操作最好是幂等的,比如查询操作、幂等的更新操作,避免重试导致重复提交数据等问题
- 重试间隔不要设置过短,否则会快速消耗资源,同时给下游服务带来不必要的压力
- 如果重试逻辑需要在多个地方复用,可以把重试逻辑封装成通用工具函数,减少重复代码