这是否是泛型的有效用例?

huangapple go评论81阅读模式
英文:

Is this a valid use case for generics?

问题

在我的代码中,我有一些重复的工作,大致如下所示。

type FP = func(arg1 string, arg2 int, arg3 []string) bool
func Decorator(fp FP, arg1 string, arg2 int, arg3 []string) {
    // 在运行 fp 之前做一些事情
    res := fp(arg1, arg2, arg3)
    // 根据 res 做一些其他事情
}

我的问题是,每次我想要添加一个具有不同数量/类型参数的新的 FP 类型时,我都必须重新定义这个方法,所以我最终在我的代码中会有 4-5 次重新定义 decorator,并且可能还需要更多次。我想用以下方式替换 decorator:

type FP = func(a ...interface{}) bool
func Decorator(fp FP, argDetails interface{}) {
    // 做一些事情
    res := fp(a)
    // 做一些其他事情
}

并且让 FP 对类型进行断言。但我不确定这样做是否会引起后续问题/是否是一个糟糕的用例,因为我看到很多关于不使用接口/会导致性能问题的内容。

英文:

Right now in my code I have a bunch of repeated work that looks something like this.

type FP = func(arg1 string, arg2 int, arg3 []string) bool
func Decorator(fp FP, arg1 string, arg2 int, arg3 []string) {
    // Do some stuff prior to running fP
    res := fp(arg1, arg2, arg3)
    // Do some other stuff depending on res
}

My problem is that I have to redefine this method every time I want to add a new FP type with differing number/type of arguments, so I end up having decorator redefined across my code 4-5 times and will probably need to do it more. I want to replace decorator with

type FP = func(a ...interface{}) bool
func Decorator(fp FP, argDetails interface{}) {
    // do stuff
    res := fp(a)
    // do other stuff
}

And having FP assert the type. But I'm unsure if this will cause issues down the line/is a bad use case as Ive seen a lot of stuff about not using interface/will cause performance issues.

答案1

得分: 3

泛型在这种情况下并不会有太大帮助,主要是因为泛型只能帮助处理类型,而不能处理参数的数量。个人而言,我会稍微以不同的方式解决这个问题:

func Decorator(fp func()) {
    // 做一些操作
    fp()
    // 做其他操作
}

使用闭包调用它:

var res someType

Decorator(func() {
    res = originalFunc(arg1, arg2, arg3)
})

这样做在调用点上可能会有一些冗长,但它适用于任何函数签名——任意数量或类型的参数和任意数量或类型的返回值。它甚至适用于方法调用。听起来你可能根据结果做一些逻辑处理,所以你还可以这样做:

func Decorator(fp func() bool) {
    // 做一些操作
    res := fp()
    // 做其他操作
}

var res someType

Decorator(func() bool {
    res = originalFunc(arg1, arg2, arg3)
    return res
})

即使被装饰的函数有多个返回值,而不仅仅是一个布尔值,或者如果你想使用其他逻辑(比如,如果它返回一个错误,你想用!= nil将其转换为布尔值),这种方法仍然有效。

英文:

Generics won't help for a few reasons, in particular that generics only help with types, not with the number of arguments. Personally I'd solve this a little differently:

func Decorator(fp func()) {
    // do stuff
   fp()
    // do other stuff
}

Calling it using a closure:

var res someType

Decorator(func() {
    res = originalFunc(arg1, arg2, arg3)
})

It's a little more verbose at the callsite but it works with absolutely any function signature - any number or type of arguments and any number or type of return values. It even works with method calls. It sounds like maybe you're doing some logic based on the result so you could also:

func Decorator(fp func() bool) {
    // do stuff
   res := fp()
    // do other stuff
}

var res someType

Decorator(func() bool {
    res = originalFunc(arg1, arg2, arg3)
    return res
})

Which will still work even if the decorated func has more return values than just that bool, or if you want to use other logic (like, say, if it returns an error you want to convert to a bool with != nil).

答案2

得分: 1

有一些模式可以帮助处理通用的中间件或装饰器。

你提到的接口实际上已经存在了,尤其是在泛型出现之前。例如,你可以在这里查看一个例子:https://gokit.io/examples/stringsvc.html

现在,由于泛型已经发布,有一些技巧可以使用泛型实现中间件而无需进行类型断言。

你可以在这里找到一个使用泛型的真实世界示例:https://github.com/bufbuild/connect-go,如果你足够努力的话。尽管我认为他们在某个地方仍然使用接口和类型断言。

其中一个关键方面是请求和响应始终只有一个值。为了解决我们问题所提出的限制,你可以将所有参数都放在一个结构体中(如果需要,也可以对响应做同样的处理)。由于一切都是结构体,现在你可以将其作为单个值传递。

下面是一个使用这些围绕泛型的思想的简单实现。

https://go.dev/play/p/fhP1mDwBSXD

func main() {
	handleUpper := decorateLogging("upper", upper)
	fmt.Println(handleUpper(context.TODO(), upperParams{msg: "hello, gopher."}))

	handleAdd := decorateLogging("add", add)
	fmt.Println(handleAdd(context.TODO(), addParams{a: 11, b: 31}))
}

type Handler[REQ any, RES any] func(ctx context.Context, req REQ) (res RES, err error)

func decorateLogging[REQ any, RES any](label string, next Handler[REQ, RES]) Handler[REQ, RES] {
	return func(ctx context.Context, req REQ) (res RES, err error) {
		defer func(ts time.Time) {
			log.Printf("label=%s success=%v took=%s", label, err == nil, time.Now().Sub(ts))
		}(time.Now())
		res, err = next(ctx, req)
		return res, err
	}
}

type upperParams struct {
	msg string
}

func upper(_ context.Context, params upperParams) (string, error) {
	s := strings.ToTitle(params.msg)
	return s, nil
}

type addParams struct {
	a int
	b int
}

func add(_ context.Context, params addParams) (int, error) {
	r := params.a + params.b
	return r, nil
}

这只是一种方法,其他答案也很好。如果你可以使用更简单的方法,我会选择那种方法。不过有时候这种模式也会很有用。

英文:

There are some patterns that can help with generic middleware or decorators.

The thing that you suggest with interface actually exists out there. Especially before generics. As example, you can look here https://gokit.io/examples/stringsvc.html

Now, since generics have been released, there are some tricks you can do with generics to achieve middleware without type assertion.

You can find a real world example using generics here: https://github.com/bufbuild/connect-go, if you look hard enough. Although I think they still use interfaces and type assertion, somewhere down the line.

One of the key aspects is that request and response are always exactly one value. To get around the limitation posed by our question, you can stick all the params in a struct (and also do the same for response, if required). Since everything is a struct, you can now pass that as a single value.

Below is a simple implementation using those ideas revolving around generics.

https://go.dev/play/p/fhP1mDwBSXD

func main() {
	handleUpper := decorateLogging("upper", upper)
	fmt.Println(handleUpper(context.TODO(), upperParams{msg: "hello, gopher."}))

	handleAdd := decorateLogging("add", add)
	fmt.Println(handleAdd(context.TODO(), addParams{a: 11, b: 31}))
}

type Handler[REQ any, RES any] func(ctx context.Context, req REQ) (res RES, err error)

func decorateLogging[REQ any, RES any](label string, next Handler[REQ, RES]) Handler[REQ, RES] {
	return func(ctx context.Context, req REQ) (res RES, err error) {
		defer func(ts time.Time) {
			log.Printf("label=%s success=%v took=%s", label, err == nil, time.Now().Sub(ts))
		}(time.Now())
		res, err = next(ctx, req)
		return res, err
	}
}

type upperParams struct {
	msg string
}

func upper(_ context.Context, params upperParams) (string, error) {
	s := strings.ToTitle(params.msg)
	return s, nil
}

type addParams struct {
	a int
	b int
}

func add(_ context.Context, params addParams) (int, error) {
	r := params.a + params.b
	return r, nil
}

This is just one way of doing it. Other answers here look also good. If you can get away with a simpler method, I would go for that. Sometimes this pattern here can come in handy though.

huangapple
  • 本文由 发表于 2023年6月6日 05:09:10
  • 转载请务必保留本文链接:https://go.coder-hub.com/76410025.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定