Search code examples
gointerfacedecorator

Which arguments for Golang decorator functions


I would like to use a setter on several methods of AdminAPI such as Update. To do so I created a method type that could match with other methods.

Should I use interface instead of func type?

type AdminAPI struct {
}

type ToAdminCtx func(ctx context.Context, req interface{}) (interface{}, error)

func (a AdminAPI) AdminM2MSetter(s ToAdminCtx) ToAdminCtx {
    return func(ctx context.Context, arg interface{}) (interface{}, error) {
        m2mPrincipal, _ := a.GetM2MPrincipal(ctx)
        ctxM2M := extlib.SetPrincipal(ctx, m2mPrincipal)
        return s(ctxM2M, arg)
    }
}

func (a AdminAPI) Update(ctx context.Context, req *ReqType) (RespType, error) {}
updateWithAdminCtx := a.adminAPI.AdminM2MSetter(s.adminAPI.Update)
// ERROR => cannot use s.adminAPI.Update (value of type func(ctx 
// context.Context, req *ReqType) (RespType, error)) as grpcAdmin.ToGetAdminCtx value in 
// argument to s.adminAPI.AdminM2MSetter

_, err := updateWithAdminCtx(ctx context.Context, req *ReqType)

Solution

  • The error you're getting is pretty self-explanatory I think:

    a.adminAPI.AdminM2MSetter(s.adminAPI.Update)
    

    This is calling

    func (a AdminAPI) AdminM2MSetter(s ToAdminCtx) ToAdminCtx {
    

    Passing in s.adminAPI.Update as the argument, which is expected to be of the type ToAdminCtx. That type you is defined as:

    type ToAdminCtx func(ctx context.Context, req interface{}) (interface{}, error)
    

    Yet your Update function's second argument is a *ReqType, and its first return value is a RespType value, and therefore Update is not a ToAdminCtx. The ToAdminCtx function type is a function that can be called with a context and literally ANY type. Your Update function cannot be guaranteed to work in all cases the ToAdminCtx function can.

    What you're looking for is a way to "wrap" any function, add do some work on the ctx argument (probably setting some value), and then pass on the call. Before go 1.19, we did this by adding some kind of wrapper types like this:

    type Wrapper struct {
        UpdateReqType *ReqType
        AnotherType *ReqType2 // for some other call you want to wrap
    }
    

    The change all the relevant functions, like your Update function to take the wrapper argument type:

    func (a AdminAPI) Update(ctx context.Context, req Wrapper) (Resp, error) {
        realReq := req.UpdateReqType // get the actual request used here
    }
    

    The response types would either be similarly wrapped and/or composed.

    Now, though, go supports generics, and this is a situation where they can be quite useful, let's change your AdminM2MSetter function to something like this:

    func AdminM2MSetter[T any, R any](s func(context.Context, T) (R, error)) func(context.Context, T) (R, error) {
        return func (ctx context.Context, arg T) (R, error) {
            m2mPrincipal, _ := a.GetM2MPrincipal(ctx)
            ctxM2M := extlib.SetPrincipal(ctx, m2mPrincipal)
            return s(ctxM2M, arg)
        }
    }
    

    This way, we only have to define this function once, but rely on the compiler to generate a tailor-made function for all the types we need. In case of your Update function, we'd do something like this:

    a.adminAPI.AdminM2MSetter[*ReqType, RespType](s.adminAPI.Update)
    

    Essentially replacing the generic T and R types with the specific types used by your Update function. Because I don't really know what functions you want to wrap in this way, I used T any, R any, but because it looks to me like you're trying to wrap request handlers of some sort, you could create your own constraints:

    type Requests interface {
        *ReqType1 | *ReqType2 | *ReqType3 // and so on
    }
    type Responses interface {
        Resp1 | Resp2 | Resp3
    }
    

    And just replace [T any, R any] with [T Requests, R Responses]