编辑
2024-01-18
Golang
00
请注意,本文编写于 817 天前,最后修改于 815 天前,其中某些信息可能已经过时。

目录

golang函数重试机制实现
前言
造轮子
V1版本
V2版本
第三方轮子

golang函数重试机制实现

前言

在编写应用程序时,有时候会遇到一些短暂的错误,例如网络请求、服务链接终端失败等,这些错误可能导致函数执行失败。
但是如果稍后执行可能会成功,那么在一些业务场景下就需要重试了,重试的概念很简单,这里就不做过多阐述了

造轮子

最近也正好在转golang语言,重试机制正好可以拿来练手,重试功能一般需要支持以下参数

  • execFunc:需要被执行的重试的函数
  • interval:重试的间隔时长
  • attempts:尝试次数
  • conditionMode:重试的条件模式,error和bool模式(这个参数用于控制传递的执行函数返回值类型检测)

V1版本

go
// myretryimpl.go package retryimpl import ( "errors" "fmt" "time" ) // 默认配置 const ( DefaultInterval = 1 * time.Second DefaultConditionMode = ConditionModeError DefaultAttempts = 1 ) // 重试条件模式 const ( ConditionModeError = "error" ConditionModeBool = "bool" ) // RetryOption 配置选项函数 type RetryOption func(retry *Retry) // Retry 重试类 type Retry struct { ExecFunc func() any // 重试的函数 interval time.Duration // 重试的间隔时长 attempts int // 重试次数 conditionMode string // 重试的条件模式,error和bool模式 } // NewRetry 构造函数 func NewRetry(opts ...RetryOption) *Retry { retry := Retry{ interval: DefaultInterval, attempts: DefaultAttempts, conditionMode: DefaultConditionMode, } for _, opt := range opts { opt(&retry) } return &retry } // WithConditionMode 设置重试条件模式,是以error还是bool形式来判断是否重试 func WithConditionMode(conditionMode string) RetryOption { return func(retry *Retry) { retry.conditionMode = conditionMode } } // WithInterval 重试的时间间隔配置 func WithInterval(interval time.Duration) RetryOption { return func(retry *Retry) { retry.interval = interval } } // WithAttempts 重试的次数 func WithAttempts(attempts int) RetryOption { return func(retry *Retry) { retry.attempts = attempts } } // Do 对外暴露的执行函数 func (r *Retry) Do(execFunc func() any) { fmt.Println("[Retry.do] begin dispatch execute func...") r.ExecFunc = execFunc r.dispatch() } // dispatch 执行函数 func (r *Retry) dispatch() { n := 0 for n < r.attempts { switch r.conditionMode { case ConditionModeError: err := r.dispatchErrorMode() if err == nil { return } case ConditionModeBool: resBool := r.dispatchBoolMode() if resBool { return } } n++ time.Sleep(r.interval) } } // dispatchErrorMode 重试任务函数返回值是error类型的调用 func (r *Retry) dispatchErrorMode() (err error) { // 既然是异常模式,那么要支持任务函数内部panic之后的recover操作,不要中断程序执行 defer func() { if r := recover(); r != nil { fmt.Println("we catch the execution function exception and recover it") err = errors.New("specified error message") } }() execResult := r.ExecFunc() if value, ok := execResult.(error); ok { return value } panic(fmt.Sprintf("got unexpect execute function response type: %T", execResult)) } // dispatchBoolMode 重试任务函数返回值是bool类型的调用 func (r *Retry) dispatchBoolMode() bool { execResult := r.ExecFunc() if value, ok := execResult.(bool); ok { return value } panic(fmt.Sprintf("got unexpect execute function response type: %T", execResult)) }

上面的这个 v1版本的实现能完成基本的重试函数需求, 接下来为上面的实现写一些单测代码:

go
package retryimpl import ( "errors" "fmt" "testing" "time" ) // TestRetry_DoFuncBoolMode 测试bool模式 // 被执行的函数如果返回false那么会进行重试,超过attempts配置的次数,则重试会停止 // 被执行的函数如果返回true那么只会执行一次,无论attempts配置多少次应该只执行一次 func TestRetry_DoFuncBoolMode(t *testing.T) { testSuites := []struct { exceptExecCount int actualExecCount int execFunResBool bool }{ {exceptExecCount: 3, actualExecCount: 0, execFunResBool: false}, {exceptExecCount: 1, actualExecCount: 0, execFunResBool: true}, } for _, testSuite := range testSuites { retry := NewRetry( WithAttempts(testSuite.exceptExecCount), WithInterval(1*time.Second), WithConditionMode(ConditionModeBool), ) retry.Do(func() any { fmt.Println("[TestRetry_DoFuncBoolMode] was called ...") testSuite.actualExecCount++ return testSuite.execFunResBool }) if testSuite.actualExecCount != testSuite.exceptExecCount { t.Errorf("[TestRetry_DoFuncBoolMode] got actualExecCount:%v != exceptExecCount:%v", testSuite.actualExecCount, testSuite.exceptExecCount) } } }

在retryimpl包下执行 go test -v

shell
go test -v # 单测运行结果输出 # === RUN TestRetry_DoFuncBoolMode # [Retry.do] begin dispatch execute func... # [TestRetry_DoFuncBoolMode] was called ... # [TestRetry_DoFuncBoolMode] was called ... # [TestRetry_DoFuncBoolMode] was called ... # [Retry.do] begin dispatch execute func... # [TestRetry_DoFuncBoolMode] was called ... # --- PASS: TestRetry_DoFuncBoolMode (3.00s) # PASS

但是也有一些缺陷,比如重试函数需要返回值的场景呢?显然上面的实现方式还不够完美。 下面进行重构

V2版本

上面的实现中,如果重试函数需要得到执行结果,那么就覆盖不到了,下面进行优化,提供支持重试返回值的方式

go
package retryimpl import ( "fmt" "time" ) // RetryOptionV2 配置选项函数 type RetryOptionV2 func(retry *RetryV2) // RetryFunc 不带返回值的重试函数 type RetryFunc func() error // RetryFuncWithData 带返回值的重试函数 type RetryFuncWithData func() (any, error) // RetryV2 重试类 type RetryV2 struct { interval time.Duration // 重试的间隔时长 attempts int // 重试次数 } // NewRetryV2 构造函数 func NewRetryV2(opts ...RetryOptionV2) *RetryV2 { retry := RetryV2{ interval: DefaultInterval, attempts: DefaultAttempts, } for _, opt := range opts { opt(&retry) } return &retry } // WithIntervalV2 重试的时间间隔配置 func WithIntervalV2(interval time.Duration) RetryOptionV2 { return func(retry *RetryV2) { retry.interval = interval } } // WithAttemptsV2 重试的次数 func WithAttemptsV2(attempts int) RetryOptionV2 { return func(retry *RetryV2) { retry.attempts = attempts } } // DoV2 对外暴露的执行函数 func (r *RetryV2) DoV2(executeFunc RetryFunc) error { fmt.Println("[Retry.DoV2] begin execute func...") retryFuncWithData := func() (any, error) { return nil, executeFunc() } _, err := r.DoV2WithData(retryFuncWithData) return err } // DoV2WithData 对外暴露知的执行函数可以返回数据 func (r *RetryV2) DoV2WithData(execWithDataFunc RetryFuncWithData) (any, error) { fmt.Println("[Retry.DoV2WithData] begin execute func...") n := 0 for n < r.attempts { res, err := execWithDataFunc() if err == nil { return res, nil } n++ time.Sleep(r.interval) } return nil, nil }

V2版本的单测代码

go
package retryimpl import ( "errors" "fmt" "testing" "time" ) // TestRetryV2_DoFunc func TestRetryV2_DoFunc(t *testing.T) { testSuites := []struct { exceptExecCount int actualExecCount int }{ {exceptExecCount: 3, actualExecCount: 0}, {exceptExecCount: 1, actualExecCount: 1}, } for _, testSuite := range testSuites { retry := NewRetryV2( WithAttemptsV2(testSuite.exceptExecCount), WithIntervalV2(1*time.Second), ) err := retry.DoV2(func() error { fmt.Println("[TestRetry_DoFuncBoolMode] was called ...") if testSuite.exceptExecCount == 1 { return nil } testSuite.actualExecCount++ return errors.New("raise error") }) if err != nil { t.Errorf("[TestRetryV2_DoFunc] retyr.DoV2 execute failed and err:%+v", err) continue } if testSuite.actualExecCount != testSuite.exceptExecCount { t.Errorf("[TestRetryV2_DoFunc] got actualExecCount:%v != exceptExecCount:%v", testSuite.actualExecCount, testSuite.exceptExecCount) } } } // TestRetryV2_DoFuncWithData func TestRetryV2_DoFuncWithData(t *testing.T) { testSuites := []struct { exceptExecCount int resMessage string }{ {exceptExecCount: 3, resMessage: "fail"}, {exceptExecCount: 1, resMessage: "ok"}, } for _, testSuite := range testSuites { retry := NewRetryV2( WithAttemptsV2(testSuite.exceptExecCount), WithIntervalV2(1*time.Second), ) res, err := retry.DoV2WithData(func() (any, error) { fmt.Println("[TestRetryV2_DoFuncWithData] DoV2WithData was called ...") if testSuite.exceptExecCount == 1 { return testSuite.resMessage, nil } return testSuite.resMessage, errors.New("raise error") }) if err != nil { t.Errorf("[TestRetryV2_DoFuncWithData] retyr.DoV2 execute failed and err:%+v", err) continue } if val, ok := res.(string); ok && val != testSuite.resMessage { t.Errorf("[TestRetryV2_DoFuncWithData] got unexcept result:%+v", val) continue } t.Logf("[TestRetryV2_DoFuncWithData] got result:%+v", testSuite.resMessage) } }

第三方轮子

retry-go

本文作者:relakkes

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!