深入掌握 lo 库的 AttemptWhile:基于 Go 泛型的可控重试机制
【免费下载链接】lo💥 A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo
lo 是一个基于 Go 1.18+ 泛型构建的 Lodash 风格函数库,其 retry.go 提供了一组面向"重试场景"的核心辅助函数。其中AttemptWhile在传统Attempt(固定次数重试)的基础上增加了"由调用方动态决定是否继续重试"的能力:函数每次执行返回(error, bool),第二个布尔返回值即可立即终止整个重试循环。本文将以 docs/data/core-attemptwhile.md 为主线,结合 retry.go 的源码实现与 retry_test.go 的测试用例,带你掌握AttemptWhile的签名语义、终止条件、边界行为,以及带间隔版本的AttemptWhileWithDelay,并给出可直接落地的实战示例。
一、函数签名与核心语义
AttemptWhile的完整签名如下(定义于 retry.go):
func AttemptWhile(maxIteration int, f func(int) (error, bool)) (int, error)它做的事情是:最多调用fN 次,直到f返回nil错误为止。与Attempt最大的区别在于第二个返回值——f返回的布尔值用于控制"是否继续尝试":
- 返回
(nil, false):成功且停止,循环立即结束; - 返回
(err, true):失败但继续尝试; - 返回
(err, false):失败且立即终止尝试——这正是AttemptWhile相对Attempt的核心增量,适用于"某种错误不可重试,必须立刻止损"的场景; - 返回
(nil, true):成功,循环同样立即结束。
返回值方面,AttemptWhile返回(int, error):int是实际执行的尝试次数(1 起始计数),error是最后一次调用f产生的错误;若最终成功,error为nil。
文档中给出的最小示例(见 docs/data/core-attemptwhile.md):
count, err := lo.AttemptWhile(5, func(i int) (error, bool) { if i == 2 { return nil, false } return errors.New("fail"), true }) // count == 3, err == nil第 0、1 次调用返回失败但允许继续;第 2 次(即第 3 次尝试)返回(nil, false),循环终止,最终count == 3、err == nil。
二、源码级实现拆解
AttemptWhile的实现非常精简,完整逻辑见 retry.go:
func AttemptWhile(maxIteration int, f func(int) (error, bool)) (int, error) { var err error var shouldContinueInvoke bool for i := 0; maxIteration <= 0 || i < maxIteration; i++ { err, shouldContinueInvoke = f(i) if !shouldContinueInvoke { // if shouldContinueInvoke is false, then return immediately return i + 1, err } if err == nil { return i + 1, nil } } return maxIteration, err }从源码结构可以提炼出几个关键行为:
- 终止优先级:每次调用后先检查
shouldContinueInvoke,只要它为false,无论err是否为nil都立即返回i + 1, err。也就是说"停止信号"的优先级高于"错误是否为 nil"。 - 成功即返回:
shouldContinueInvoke == true且err == nil时,返回i + 1, nil,剩余迭代次数被放弃。 - 循环上限:
maxIteration <= 0表示"无限重试",直到f返回nil或false为止;否则最多执行maxIteration次。 - 耗尽兜底:如果循环因达到
maxIteration上限而自然退出(从未成功、也从未返回false),返回(maxIteration, err),其中err是最后一次失败的 error。 - 索引从 0 开始:
f收到的参数i从 0 起递增,而返回的计数值i + 1是 1 起始的实际尝试次数——这与 core-attempt.md 中Attempt的计数约定保持一致。
与兄弟函数 Attempt、AttemptWithDelay 的对比
AttemptWhile属于 lo 重试家族中的"可控终止"分支,与之配套的还有三个函数,均位于 retry.go:
| 函数 | 签名要点 | 独特能力 |
|---|---|---|
Attempt | f func(index int) error | 仅凭error是否nil决定是否继续 |
AttemptWithDelay | f func(index int, duration time.Duration) error | 每次失败后休眠delay,并回传已耗时 |
AttemptWhile | f func(int) (error, bool) | 第二个返回值可立即终止重试 |
AttemptWhileWithDelay | f func(int, time.Duration) (error, bool) | 兼具布尔终止与间隔休眠、耗时回传 |
可见AttemptWhile是Attempt的"带停止开关"升级版,而AttemptWhileWithDelay(retry.go)则在此基础上再加入time.Duration间隔与累计耗时返回,用于需要限速或观测延迟的重试场景。
三、边界行为:从测试用例看真实语义
retry_test.go 中的TestAttemptWhile用 7 组表驱动用例完整覆盖了各种边界情况,是理解该函数语义最直接的依据:
- 始终成功(
maxIteration: 42,每次返回(nil, true)):第 1 次尝试即成功,expectedIter == 1; - 多次失败后成功(第 6 次成功):
expectedIter == 6; - 耗尽上限仍失败(
maxIteration: 2,且直到i == 5才成功):expectedIter == 2且返回最后一次的错误; - 无限重试直到成功(
maxIteration: 0,第 42 次才成功):expectedIter == 43,证明maxIteration <= 0确实代表"不限次数"; - 提前止损且无错误(返回
(nil, false)):expectedIter == 6,expectErr == false; - 首次调用即止损:
expectedIter == 1,函数只被调用了一次; - 恰好在上限前一跳终止(
maxIteration: 42,i == 41返回(nil, true)):expectedIter == 42。
注意测试中expectErr的判断使用的是is.ErrorIs(gotErr, err),即错误是通过errors.New创建的原生错误,可直接用errors.Is比对——这也提示我们在实际业务中应优先返回可被errors.Is识别的哨兵错误或包装错误,便于调用方精确判断重试结果。
四、实战:区分可重试与不可重试错误
AttemptWhile最有价值的应用场景是"区分错误的严重程度":瞬时故障(网络抖动、上游 5xx)值得重试,而参数错误、鉴权失败等确定性错误重试多少次都一样,应当立即终止。
README 中的官方示例(README.md)展示了典型写法:
count1, err1 := lo.AttemptWhile(5, func(i int) (error, bool) { err := doMockedHTTPRequest(i) if err != nil { if errors.Is(err, ErrBadRequest) { // 假设 ErrBadRequest 是不可恢复的致命错误 return err, false // 第二个返回值置 false,立即终止重试 } return err, true } return nil, false })这里的关键手法:
- 用
errors.Is(err, ErrBadRequest)识别"致命错误",返回(err, false)直接止损,不再浪费剩余 4 次尝试; - 普通临时错误返回
(err, true),交给循环继续重试; - 成功时返回
(nil, false)(或(nil, true)均可,因为err == nil本身就会终止循环)。
执行完毕后,count1记录了实际尝试次数,可作为重试日志的观测指标;若最终返回的err1非nil,可进一步用errors.Is判断其属于"重试耗尽"还是"被致命错误打断"。
带间隔版本:AttemptWhileWithDelay
如果每次重试之间需要停顿(例如避免对下游接口的突刺式请求),应使用AttemptWhileWithDelay,其签名见 docs/data/core-attemptwhilewithdelay.md:
func AttemptWhileWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) (error, bool)) (int, time.Duration, error)与AttemptWhile相比多出两个变化:
- 每次失败后(且还有剩余次数时)会
time.Sleep(delay)再进入下一次尝试; f的第二个参数回传自循环开始以来的累计耗时xtime.Since(start),可用于慢请求观测;- 返回三元组
(int, time.Duration, error),其中time.Duration是总耗时。
官方示例(README.md):
count1, time1, err1 := lo.AttemptWhileWithDelay(5, time.Millisecond, func(i int, d time.Duration) (error, bool) { err := doMockedHTTPRequest(i) if err != nil { if errors.Is(err, ErrBadRequest) { return err, false } return err, true } return nil, false })retry_test.go 中TestAttemptWhileWithDelay的用例进一步验证了时间语义:例如maxIteration: 0且第 10 次才成功时,expectedDelta为100 * time.Millisecond(10 次间隔 × 10ms),并用assert.InDelta以 5ms 的 epsilon 容忍调度误差;最后一次尝试成功后不再执行额外休眠(i+1 < maxIteration才 sleep)。
值得注意的是,源码中时间相关操作(xtime.Now、xtime.Sleep、xtime.Since)都通过 internal/xtime 封装,该内部包提供了可替换的时钟实现,便于在测试中注入假时钟(见 internal/xtime/fake.go),这也是库本身可测性设计的一部分。
五、注意事项与使用建议
综合源码与测试,使用AttemptWhile时有几点值得留意:
maxIteration <= 0是无限重试:与Attempt一致(见 retry.go 的注释),传入 0 或负数时必须确保f最终会返回nil或false,否则将死循环。生产环境建议始终传入正数上限,并配合超时控制。- 停止信号优先于错误判断:返回
(err, false)时函数以"携带错误的提前终止"结束,调用方应通过count < maxIteration(或与maxIteration比较)来区分"止损退出"与"次数耗尽"两种失败形态。 - 错误应可被识别:尽量返回哨兵错误或使用
fmt.Errorf("...: %w", err)包装,方便事后用errors.Is/errors.As分析失败原因。 - 回调应尽量幂等:由于
f可能被多次调用且带有副作用(例如上面的doMockedHTTPRequest),务必保证每次尝试是独立可重入的,避免部分成功的状态污染后续尝试。 - 需要更复杂退避策略时:
AttemptWhile系列只支持固定间隔;若需要指数退避、抖动等高级策略,README 也建议结合专用重试库(如 cenkalti/backoff)使用。
六、快速体验与源码导航
你可以直接在 Go 1.18+ 项目中引入 lo 并体验该函数:
import "github.com/samber/lo" count, err := lo.AttemptWhile(3, func(i int) (error, bool) { if i == 2 { return nil, false } return errors.New("fail"), true })本仓库中的相关文件索引:
- 实现源码:retry.go(
AttemptWhile与AttemptWhileWithDelay) - 单元测试:retry_test.go(边界行为全覆盖的表驱动用例)
- 官方文档页:docs/docs/core/retry.md(Retry 分类汇总页)、core-attemptwhile.md、core-attemptwhilewithdelay.md
- README 实战示例:README.md
- 时钟抽象:internal/xtime
AttemptWhile把"是否继续重试"的决策权从固定次数逻辑交还给了业务代码,用极小的 API 代价换来了对重试行为的精细控制。对于"部分错误可重试、部分错误必须立即失败"的真实业务场景,它是最直接、最不易出错的 Go 泛型重试原语。
【免费下载链接】lo💥 A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)项目地址: https://gitcode.com/GitHub_Trending/lo/lo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考