一、一个简单的业务背景
我们有个业务层叫service, 两个方法,它们的返回值不同。
虽然不太一样,但是结构一样, 都是 func() (T, error)
Go
type Service struct{}
func (s *Service) GetUser() (string, error) {
return "alice", nil
}
func (s *Service) GetAge() (int, error) {
return 18, nil
}
这时候假如我们需要一个"通用重试中间件",就是在业务方法调用失败的时候, 能重试一次。对应到真实业务,就好比我们每个方法都是调用第三方平台接口进行操作,如果操作失败能重试,或者token失效,能刷新一次token在重试等等。
二、非泛型方式实现
我们可以实现一个通用的中间件,它的参数就是上面的业务方法,先去执行业务方法,如果失败,在执行一次。
我们就得抽象出下面的函数类型
Go
type BizFuncAny func() (any, error)
中间件
Go
func WithRetry(fn BizFunc) BizFunc {
return func() (any, error) {
resp, err := fn()
if err == nil {
return resp, nil
}
// 模拟:失败后重试一次
return fn()
}
}
当要调用GetUser(),我们要手动包一层才能把GetUser()变为符合上面定义的函数类型
Go
getUser := func() (any, error) {
return svc.GetUser()
}
使用
Go
func main() {
svc := &Service{}
// GetUser 被包成 BizFunc
getUser := WithRetry(func() (any, error) {
return svc.GetUser()
})
// GetAge 被包成 BizFunc
getAge := WithRetry(func() (any, error) {
return svc.GetAge()
})
u, _ := getUser()
a, _ := getAge()
// ⚠️ 必须类型断言
fmt.Println(u.(string))
fmt.Println(a.(int))
}
这个方式有几个缺点:
1、对于svc.GetUser等业务方法必须包一层,这样才能符合WithRetry的参数的函数签名。所以每次对新的业务方法使用的时候,都要手写闭包。
2、返回值变为any,如果我们要处理这个返回值,就得进行类型断言。
3、编译期无法发现类型错误。
4、改 DTO 会导致运行时 panic。
三、使用泛型的简单例子
我们定义一个"业务函数类型",泛型Resp:表示不同业务返回不同类型。
Go
type BizFunc[Resp any] func() (Resp, error)
实现一个"通用重试中间件"
Go
func WithRetry[Resp any](
fn BizFunc[Resp],
) BizFunc[Resp] {
return func() (Resp, error) {
resp, err := fn()
if err == nil {
return resp, nil
}
// 这里假装"重试一次"
return fn()
}
}
使用这个泛型函数
Go
func main() {
svc := &Service{}
getUser := WithRetry(svc.GetUser)
getAge := WithRetry(svc.GetAge)
u, _ := getUser()
a, _ := getAge()
fmt.Println(u, a) // alice 18
}