Golang函数重试机制实现 (手把手造轮子)

Golang函数重试机制实现

前言

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

造轮子

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

  • execFunc:需要被执行的重试的函数
  • interval:重试的间隔时长
  • attempts:尝试次数
  • conditionMode:重试的条件模式,error和bool模式(这个参数用于控制传递的执行函数返回值类型检测)
V1版本
// 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版本的实现能完成基本的重试函数需求, 接下来为上面的实现写一些单测代码:

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

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

但是也有一些缺陷,比如重试函数需要返回值的场景呢?显然上面的实现方式还不够完美。
下面进行重构
上面的实现中,如果重试函数需要得到执行结果,那么就覆盖不到了,下面进行优化,提供支持重试返回值的方式

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版本的单测代码

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

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员阿江Relakkes

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值