Go functional programming: higher order function

Time:2021-8-13

In the process of request processing, the application will accept and process the request, and then return the response result. In this process, there are also some general functions, such as authentication, monitoring and link tracking. Many RPC frameworks will provide concepts such as middleware or interceptor to support many of the functions mentioned above in a pluggable manner. Taking grpc as an example, the working principle is shown in the figure:

Go functional programming: higher order function

The server interface is as follows:

func UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error)

func StreamServerInterceptor (srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error

It can be seen that the interface clearly defines the input parameters and output results. If we want to implement a component by ourselves, we need to support users to input specific configurations. Is there any way to do this?

The answer is yes.

Higher-order function

Before understanding a specific solution, you need to understand a concept calledHigher order function

Higher order functions are those that support at least one of the following specific functions:

  1. Taking one or more functions as parameters (i.e. process parameters),
  2. Returns a function as its result

The second point is the required characteristics. Taking the current limiter as an example, it supports the introduction of custom current limiters. At this time, it is necessary to define a high-order function with current limiter as the parameter, and then the returned function is the interceptor required by the framework, and use the incoming current limiter in the interceptor function to judge whether current limiting is required. The specific implementation of current limiting interceptor according to the interface is as follows:

type Limiter interface {
    Limit(key string) bool
}

func UnaryServerInterceptor(limiter Limiter) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        if limiter.Limit(info.FullMethod) {
            return nil, status.Errorf(codes.ResourceExhausted, "%s is rejected by grpc_ratelimit middleware, please retry later.", info.FullMethod)
        }
        return handler(ctx, req)
    }
}

...

At present, the parameters passed in are fixed, which can be implemented in this way. Further, if the use is complex, it can be expected that more parameters will be added in the future in addition to the currently determined parameters. This requires that the currently designed interface needs to have good scalability. Is there any way?

The answer is also yes.

Functional Options

Not surprisingly, we use the first point of high-order functions. This programming mode has a specific name: functional options.

First, define the structure for the passed in parameters

type options struct {
    byMethod  bool
    byUser    bool
    byClientIP bool
}

Next, define a function type:

type Option func(*Options)

Again, define a set of functions that modify the configuration

func ByMethod(m bool) Option {
    return func(o *options) {
        o.byMethod = m
    }
}

func ByUser(u bool) Option {
    return func(o *options) {
        o.byUser = u
    }
}

func ByClientIP(c bool) Option {
    return func(o *options) {
        o.byClientIP = c
    }
}

Finally, modify the provided interceptor to:

func UnaryServerInterceptor(limiter Limiter, opts ...Option) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        default := options {
            byMethod: true,
            byUser: false,
            byClientIP: false,
        }
        for _, opt := range opts {
            opt(&default)
        }
        ...
        return handler(ctx, req)
    }
}

If so, you have an extendable interceptor that supports custom parameters.

last

Make a summary and make a point:

  1. Higher order functions do not belong to a specific programming language. Other languages, such as C + +, also support similar features.
  2. As an architect, do you need to know the implementation details? The answer is yes. Otherwise, what can be used to support the so-called scalability of design in a specific environment.

Author:cyningsun
Article address: https://www.cyningsun.com/07-…
Copyright notice: unless otherwise stated, all articles in this blog adopt CC by-nc-nd 3.0 CN license agreement. Reprint please indicate the source!