- 开发工具
【免费下载链接】lancet
A comprehensive, efficient, and reusable util function library of Go.
retry是 Go 通用工具库 lancet 提供的重试执行包,用于"反复执行一个函数直到它成功,或被 context 取消"(retry/retry.go)。在分布式系统、网络请求、数据库操作等易失败场景中,合理的重试次数、重试间隔与退避(backoff)策略是提升稳定性的关键。阅读本文后,你将掌握 lancet retry 包的完整 API、Option配置模式、线性/指数/自定义三类退避策略的实现原理,以及如何用context优雅地中断重试,并能够直接落地到自己的项目中。
一、包定位与设计概述
lancet 将工具函数按包组织,retry包专注于"重试执行"这一横切关注点。其核心设计是函数式选项(Functional Options)模式:所有可调参数都通过返回Option的构造函数注入,主函数Retry只负责执行循环。这种设计让调用方可以按需组合配置,且未来新增配置项不会破坏既有调用。
包内全部符号均可在 retry/retry.go 中找到,其中:
RetryConfig是内部配置结构体,持有context、retryTimes与backoffStrategy三个字段;Option是func(*RetryConfig)类型,所有RetryXxx/Context构造函数都返回它;BackoffStrategy是决定两次重试之间等待多久的接口,是包内扩展性的核心。
二、安装与导入
lancet 当前版本(v2.x)要求 Go 1.18 及以上(见 go.mod 中go 1.18),安装命令:
go get github.com/duke-git/lancet/v2导入方式(以模块路径github.com/duke-git/lancet/v2为准):
import ( "github.com/duke-git/lancet/v2/retry" )三、API 总览
retry包对外暴露的符号如下表:
| 符号 | 类型 | 作用 |
|---|---|---|
Retry | 函数 | 重试执行RetryFunc,直到成功或被 context 取消 |
RetryFunc | 类型 | 被重试执行的函数,签名func() error |
RetryTimes | 函数 | 设置重试次数,默认 5 次 |
RetryDuration | 函数 | 设置两次重试间隔(见下文"历史说明") |
Context | 函数 | 设置重试 context,可取消重试 |
BackoffStrategy | 接口 | 定义计算退避间隔的方法CalculateInterval() time.Duration |
RetryWithCustomBackoff | 函数 | 设置任意自定义退避策略 |
RetryWithLinearBackoff | 函数 | 设置线性退避策略 |
RetryWithExponentialWithJitterBackoff | 函数 | 设置带随机抖动(jitter)的指数退避策略 |
其中Retry是唯一的入口,其余函数均为配置构造函数。README 的 retry 小节 也给出了同样的函数清单与中文说明,可作为速查参考。
关于 RetryDuration 的历史说明
文档索引中保留了RetryDuration(d time.Duration)条目(默认 3 秒),但从当前源码结构看,该函数在 retry/retry.go 中已不存在,取而代之的是RetryWithLinearBackoff(interval time.Duration)。如果你需要"固定间隔重试",请直接使用RetryWithLinearBackoff;若完全不指定任何退避策略,Retry内部默认采用间隔 3 秒的线性退避(常量DefaultRetryLinearInterval = time.Second * 3,见 retry/retry.go)。
四、核心入口Retry的源码级原理
Retry的完整实现位于 retry/retry.go,核心逻辑如下:
func Retry(retryFunc RetryFunc, opts ...Option) error { config := &RetryConfig{ retryTimes: DefaultRetryTimes, // 默认 5 次 context: context.TODO(), } for _, opt := range opts { opt(config) // 依次应用所有 Option } if config.backoffStrategy == nil { config.backoffStrategy = &linear{ interval: DefaultRetryLinearInterval, // 默认 3 秒 } } var i uint var lastErr error for i < config.retryTimes { lastErr = retryFunc() if lastErr == nil { return nil // 成功立即返回 } if i < config.retryTimes-1 { // 仅非最后一次重试时等待 select { case <-time.After(config.backoffStrategy.CalculateInterval()): case <-config.context.Done(): return errors.New("retry is cancelled") } } i++ } // 通过反射与 runtime 获取被重试函数的名称 funcPath := runtime.FuncForPC(reflect.ValueOf(retryFunc).Pointer()).Name() lastSlash := strings.LastIndex(funcPath, "/") funcName := funcPath[lastSlash+1:] return fmt.Errorf("function %s run failed after %d times retry, last error: %w", funcName, i, lastErr) }从实现中可以提炼出几个关键事实:
- 成功即返回:只要
retryFunc返回nil,循环立即结束并返回nil,后续次数不会浪费; - 最后一次失败不等待:
i < config.retryTimes-1的判断确保"第 N 次尝试失败后不再 sleep,直接返回错误",避免多余的无效等待; - 等待可被 context 打断:间隔等待通过
select同时监听time.After与context.Done(),context 被取消时立即返回errors.New("retry is cancelled"); - 错误信息自带函数名:通过
runtime.FuncForPC+reflect.ValueOf(...).Pointer()解析出被重试函数的名称,最终错误格式为function <函数名> run failed after <次数> times retry, last error: <原始错误>。注意:文档示例中输出只写到了...after 2 times retry,是省略写法;实际返回的错误通过%w包装了最后一次错误,可用errors.Is/errors.As展开判断(retry/retry.go)。
默认值速查
| 配置项 | 默认值 | 定义位置 |
|---|---|---|
| 重试次数 | 5(DefaultRetryTimes) | retry/retry.go |
| 退避间隔 | 3 秒(DefaultRetryLinearInterval) | retry/retry.go |
| context | context.TODO() | retry/retry.go |
五、Option 配置项逐个击破
所有 Option 构造函数都遵循同一模式:返回一个闭包,在Retry内部被应用到RetryConfig上。
1. RetryTimes(n uint):控制重试次数
设置最大重试次数(含首次尝试)。默认 5 次。示例:当函数第 3 次才成功、而次数上限为 2 时,将得到失败错误:
package main import ( "errors" "fmt" "github.com/duke-git/lancet/v2/retry" ) func main() { number := 0 increaseNumber := func() error { number++ if number == 3 { return nil } return errors.New("error occurs") } err := retry.Retry(increaseNumber, retry.RetryTimes(2)) if err != nil { fmt.Println(err) } // Output 形如: // function main.main.func1 run failed after 2 times retry, last error: error occurs }2. Context(ctx):优雅地取消重试
重试可能长时间空转,Context选项允许外部通过context.CancelFunc随时中断。示例中函数始终返回错误,但第 4 次调用时主动cancel(),Retry在等待阶段感知 context 取消并立即返回:
package main import ( "context" "errors" "fmt" "time" "github.com/duke-git/lancet/v2/retry" ) func main() { ctx, cancel := context.WithCancel(context.TODO()) number := 0 increaseNumber := func() error { number++ if number > 3 { cancel() } return errors.New("error occurs") } retry.Retry(increaseNumber, retry.RetryWithLinearBackoff(time.Microsecond*50), retry.Context(ctx), ) fmt.Println(number) // Output: // 4 }同理,你也可以在重试函数外启动一个 goroutine 在超时后调用cancel(),实现"超时即停止重试"。
六、Backoff 退避策略:重试间隔的艺术
重试如果"失败即立刻重试",往往会在服务短暂故障时形成请求风暴。退避策略决定了每次重试前等待多久。retry包以接口方式抽象了这一行为:
// BackoffStrategy 定义计算退避间隔的方法。 type BackoffStrategy interface { // CalculateInterval 返回下一次重试应等待的时间。 CalculateInterval() time.Duration }Retry主循环会在每次等待时调用CalculateInterval(),因此该接口天然支持"间隔随时间动态变化"的策略。
1. 线性退避(RetryWithLinearBackoff)
固定间隔等待,策略内部结构体为linear,CalculateInterval恒定返回设定值(retry/retry.go):
func (l *linear) CalculateInterval() time.Duration { return l.interval }使用示例:
err := retry.Retry(increaseNumber, retry.RetryWithLinearBackoff(time.Microsecond*50))参数校验:interval <= 0时直接panic("programming error: retry interval should not be lower or equal to 0")(retry/retry.go)。
2. 带抖动的指数退避(RetryWithExponentialWithJitterBackoff)
指数退避让间隔按倍数增长,适合应对流量尖峰;"抖动(jitter)"则引入随机量,避免多个客户端在同一时刻集体重试造成"惊群"。签名:
func RetryWithExponentialWithJitterBackoff(interval time.Duration, base uint64, maxJitter time.Duration) Optioninterval:初始间隔;base:增长基数;maxJitter:最大抖动幅度。
实现上有两条路径(retry/retry.go):
base为偶数时:采用位运算优化,shifter = log2(base),间隔每次左移shifter位(等价于乘以 base,但更快),对应结构体shiftExponentialWithJitter;base为奇数时:采用乘法,间隔每次乘以base,对应结构体exponentialWithJitter。
两者计算逻辑一致,返回"当前间隔 + 随机抖动":
func (e *exponentialWithJitter) CalculateInterval() time.Duration { current := e.interval e.interval = e.interval * e.base return current + jitter(e.maxJitter) }jitter的实现是:在[1, maxJitter]范围内取随机数;若maxJitter为 0 则不加抖动(retry/retry.go):
func jitter(maxJitter time.Duration) time.Duration { if maxJitter == 0 { return 0 } return time.Duration(rand.Int63n(int64(maxJitter)) + 1) }使用示例:
err := retry.Retry(increaseNumber, retry.RetryWithExponentialWithJitterBackoff(time.Microsecond*50, 2, time.Microsecond*25))参数校验:interval <= 0或maxJitter < 0时panic(retry/retry.go)。
3. 完全自定义(RetryWithCustomBackoff)
如果内置策略不满足需求,实现BackoffStrategy接口即可。例如自定义一个"固定间隔 + 1"的策略:
package main import ( "errors" "fmt" "time" "github.com/duke-git/lancet/v2/retry" ) type ExampleCustomBackoffStrategy struct { interval time.Duration } func (c *ExampleCustomBackoffStrategy) CalculateInterval() time.Duration { return c.interval + 1 } func main() { number := 0 increaseNumber := func() error { number++ if number == 3 { return nil } return errors.New("error occurs") } err := retry.Retry(increaseNumber, retry.RetryWithCustomBackoff(&ExampleCustomBackoffStrategy{interval: time.Microsecond * 50})) if err != nil { return } fmt.Println(number) // Output: // 3 }注意:RetryWithCustomBackoff传入nil策略会直接panic(retry/retry.go),因此自定义策略应始终以非空实例传入。
七、测试验证与运行
retry包对每个导出函数都提供了单元测试与可运行示例,测试位于 retry/retry_test.go,示例位于 retry/retry_example_test.go。在仓库根目录运行:
go test ./retry/...可复现全部验证。测试用例覆盖了关键行为,可直接作为你理解语义的"活文档":
| 测试函数 | 验证点 |
|---|---|
TestRetryFailed | 始终失败时执行满默认 5 次,返回错误且errors.Is(err, customError)为真 |
TestRetrySucceeded/TestRetryOneShotSucceeded | 第 5 次成功 / 首次即成功时提前返回nil |
TestSetRetryTimes | RetryTimes(3)时恰好执行 3 次后失败 |
TestCancelRetry | context 取消后停止重试,执行次数为 4 |
TestRetryWithExponentialSucceeded/TestRetryWithExponentialShiftSucceeded | base 为 3(乘法)与 base 为 4(位移)两条指数路径均正常 |
TestRetryWitCustomBackoffOneShotSucceeded | 自定义策略生效 |
这些测试同时印证了前文的分析:重试次数默认 5、最后一次失败不等待、context 取消立即返回、错误可用errors.Is解包。
八、实战注意事项
- 重试函数必须是幂等的:
retryFunc会被反复调用,若函数本身有副作用(如写库、发消息),应确认重复执行不会造成业务错误; - 用
errors.Is判断失败原因:最终错误通过%w包装了最后一次lastErr,可用errors.Is(err, 目标错误)或errors.As提取,而不是做字符串匹配; - 生产环境建议显式指定退避策略:不传退避选项时默认是 3 秒固定间隔,高并发场景下更推荐
RetryWithExponentialWithJitterBackoff,利用 jitter 打散重试节奏; - 配合 context 设置总超时:重试次数 × 间隔可能很长,建议将带超时的 context 通过
Context(ctx)注入,保证整体耗时可控; - 参数非法会 panic:
interval <= 0、maxJitter < 0、backoffStrategy == nil均触发 panic,属于编程错误(programming error),应在调用前自行校验。
综上,lancet 的retry包以极小的 API 面提供了完整的重试闭环——次数控制、context 取消、三种退避策略与自定义扩展点,配合源码中清晰的实现与完备的测试,是 Go 项目引入重试能力时成本最低的选择之一。深入阅读 retry/retry.go 与 retry/retry_test.go 即可获得全部实现细节。
- 开发工具
【免费下载链接】lancet
A comprehensive, efficient, and reusable util function library of Go.
相关推荐
Higress model-mapper 插件:基于 LLM 协议实现 AI 模型参数映射与统一路由
Higress model mapper 插件:基于 LLM 协议实现 AI 模型参数映射与统一路由 model mapper 是 Higress AI 网关中
开发工具Got 重试机制完全指南:retry 配置、指数退避与 Retry-After 实战解析
Got 重试机制完全指南:retry 配置、指数退避与 Retry After 实战解析 本篇指南围绕 Got(当前仓库为 gh_mirrors/go/got
后端网络Akka.NET 监督机制(Supervision)完全指南:从容错策略到 Backoff 指数退避
Akka.NET 监督机制(Supervision)完全指南:从容错策略到 Backoff 指数退避 Akka.NET 的监督(Supervision)是 Ac
后端微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考