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.
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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With