This article wants to talk about Go’s decorator pattern, pipeline(Filter) pattern, and middleware implementations in common Web frameworks.

Decoration pattern

Decoration pattern is a common design pattern in the field of object-oriented programming. It is a design pattern that dynamically adds new behavior to a class. In terms of functionality, decorated patterns are more flexible than subclassing, so you can add functionality to an object rather than the entire class.

Sometimes I call it the Onion model, where the tenderest, most central part of the onion is the original object, and then it has a layer of business logic on it, and then it can have another layer of business logic on it.

The principle is that adding a modifier class wraps around the original class, usually by taking the original object as an argument to the modifier class constructor. The decorator class implements new functionality, but it can call methods directly from the original class where the new functionality is not needed. Unlike the adapter pattern, the decorator pattern’s decorator class must have the same interface as the original class. It dynamically adds the behavior of an object at run time, executing specific logic before and after the execution of the wrapped object, whereas the proxy mode mainly controls the internal behavior of the object, which is generally determined at the compiler.

For the Go language, since functions are of the first class, wrapped objects can also be functions, such as the most common HTTP example:

func log(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Println("Before")
    h.ServeHTTP(w, r)
    log.Println("After")
  })
}Copy the code

The print log decorator is provided to print a log before and after the actual wrap object is executed. Because the HTTP standard library can provide router as a function (http.handlerfunc) or as a struct (http.handler), there are two implementations of modifiers (modifiers).

Generic modifiers

Left Ear Mouse provides generic-like modifiers via reflection in his Go language modifier programming article:

func Decorator(decoPtr, fn interface{}) (err error) {
        var decoratedFunc, targetFunc reflect.Value
 
        decoratedFunc = reflect.ValueOf(decoPtr).Elem()
        targetFunc = reflect.ValueOf(fn)
 
        v := reflect.MakeFunc(targetFunc.Type(),
                func(in []reflect.Value) (out []reflect.Value) {
                        fmt.Println("before")
                        out = targetFunc.Call(in)
                        fmt.Println("after")
                        return
                })
 
        decoratedFunc.Set(v)
        return
}Copy the code

Stackoverflow also has a q&A that provides a decorator pattern for reflection implementation class generics, which is basically similar.

func Decorate(impl interface{}) interface{} { fn := reflect.ValueOf(impl) //What does inner do ? What is this codeblock ? inner := func(in []reflect.Value) []reflect.Value { //Why does this return the same type as the parameters passed to the  function ? Does this mean this decorator only works for fns with signature func (arg TypeA) TypeA and not func (arg TypeA) TypeB ? f := reflect.ValueOf(impl) fmt.Println("Stuff before") // ... ret := f.Call(in) //What does call do ? Why cant we just use f(in) ? fmt.Println("Stuff after") // ... return ret } v := reflect.MakeFunc(fn.Type(), inner) return v.Interface() }Copy the code

Of course, saelo provided this GIST 14 years ago, but it had zero star, zero fork, and I contributed a fork.

Pipeline mode

A responsibility chain is one of GoF’s 23 design patterns. It consists of a set of commands and a series of handler objects, each of which defines the business logic of the command to be processed. The rest of the commands are handled by other processing objects. So the whole business you look like if… else if … else if ……. else … The handler is also responsible for distributing the remaining commands to other handlers, as endif logic dictates. The chain of responsibilities can be either linear or a complex tree topology.

A pipeline is an architectural pattern that consists of a chain of processing units, which can be processes, threads, fibers, functions, etc. After each processing unit in the chain finishes processing, it passes the result to the next one. The processing unit is also called a filter, so this pattern is also called Pipes and Filters Design Pattern.

Different from the responsibility chain pattern, the responsibility chain design pattern is different. The responsibility chain design pattern is a behavior design pattern, while pipeline is an architecture pattern. Secondly, pipeline processing unit range is very wide, large to process small function, can form pipeline design pattern; The third narrow sense of pipeline processing is one-way linear, there will be no branching tree topology; The fourth is for a processing data, each processing unit of the pipeline will process this data (or the processing results of the previous processing unit).

The Pipeline pattern is often used to implement middleware, such as filter in Java Web, middleware in Go Web framework. Let’s take a look at the various technologies that middleware implements for the Go Web Framework.

Middleware for the Go Web Framework

In this section we look at the ways in which middleware for a Web framework can be implemented.

The web middleware we are talking about here is that after receiving the user’s message, a series of pre-processing is carried out first, and then it is handed to the actual HTTP.handler for processing, and the result may need a series of subsequent processing.

Although each framework still implements the middleware of pipeline through decorator, each framework still has its own style for the processing of middleware, the main difference is When, Where and How.

Configuration during initialization

Through the decorator pattern in the previous section, we can implement the Pipeline pattern.

Take a look at xie da’s Implementation in the BeeGo framework:

// MiddleWare function for http.Handler type MiddleWare func(http.Handler) http.Handler // Run beego application. func (app *App) Run(mws ... MiddleWare) { ...... app.Server.Handler = app.Handlers for i := len(mws) - 1; i >= 0; i-- { if mws[i] == nil { continue } app.Server.Handler = mws[i](app.Server.Handler) } ...... }Copy the code

The middleware is wrapped up when the application starts, and the application only needs to end up holding the final handler.

Use the Filter array

Some Go Web frameworks implement middleware techniques using filter arrays (strictly speaking, slices).

Let’s look at an example of the GIN framework implementing middleware.

Data structure:

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
// Last returns the last handler in the chain. ie. the last handler is the main own.
func (c HandlersChain) Last() HandlerFunc {
	if length := len(c); length > 0 {
		return c[length-1]
	}
	return nil
}Copy the code

Configuration:

func (group *RouterGroup) Use(middleware ... HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() }Copy the code

Because the middleware is also HandlerFunc, it can be handled as a handler.

Let’s take a look at an example of the Echo framework implementing middleware.

// Pre adds middleware to the chain which is run before router. func (e *Echo) Pre(middleware ... MiddlewareFunc) { e.premiddleware = append(e.premiddleware, middleware...) } // Use adds middleware to the chain which is run after router. func (e *Echo) Use(middleware ... MiddlewareFunc) { e.middleware = append(e.middleware, middleware...) } func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Acquire context c := e.pool.Get().(*context) c.Reset(r, w) h := NotFoundHandler if e.premiddleware == nil { e.findRouter(r.Host).Find(r.Method, getPath(r), c) h = c.Handler() h = applyMiddleware(h, e.middleware...) } else { h = func(c Context) error { e.findRouter(r.Host).Find(r.Method, getPath(r), c) h := c.Handler() h = applyMiddleware(h, e.middleware...) return h(c) } h = applyMiddleware(h, e.premiddleware...) }... }Copy the code

The Echo framework assembles pipelines in real time as each request is processed, which means you can change the middleware usage on the fly.

Using a linked list

The IRIS framework is implemented using links

type WrapperFunc func(w http.ResponseWriter, r *http.Request, firstNextIsTheRouter http.HandlerFunc) func (router *Router) WrapRouter(wrapperFunc WrapperFunc) { if wrapperFunc == nil  { return } router.mu.Lock() defer router.mu.Unlock() if router.wrapperFunc ! = nil { // wrap into one function, from bottom to top, end to begin. nextWrapper := wrapperFunc prevWrapper := router.wrapperFunc wrapperFunc = func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { if next ! = nil { nexthttpFunc := http.HandlerFunc(func(_w http.ResponseWriter, _r *http.Request) { prevWrapper(_w, _r, next) }) nextWrapper(w, r, nexthttpFunc) } } } router.wrapperFunc = wrapperFunc }Copy the code

It can be seen that Iris is basically similar to Beego in this way, but the difference is that it can carry out different decorations for different routers, and can dynamically add decorators when running, but cannot dynamically delete them.

Functional implementation

This method is basically the linked list method, and the difference from iris method is that it implements the function of chain call.

The chain call function is useful in many places. For example, the Builder design pattern is most commonly used to initialize chain calls. We can also use it to implement chain continuous decorator wraps.

My impression was seen at the beginning of an article to introduce this way, but in writing this article in the search for two days but could not find articles in the impression, so I wrote a decorator chain calls, just the prototype or did not achieve the go web framework implementation of the middleware, in fact, the way of the realization of the various middleware is good enough.

In Go, functions are of the first class, and you can use higher-order functions that take functions as arguments or return values.

It is interesting to note that functions can also have methods themselves, and you can use this feature to make chained calls to functions.

In the following example,

type Fn func(x, y int) int
func (fn Fn) Chain(f Fn) Fn {
    return func(x, y int) int {
        fmt.Println(fn(x, y))
        return f(x, y)
    }
}
func add(x, y int) int {
    fmt.Printf("%d + %d = ", x, y)
    return x + y
}
func minus(x, y int) int {
    fmt.Printf("%d - %d = ", x, y)
    return x - y
}
func mul(x, y int) int {
    fmt.Printf("%d * %d = ", x, y)
    return x * y
}
func divide(x, y int) int {
    fmt.Printf("%d / %d = ", x, y)
    return x / y
}
func main() {
    var result = Fn(add).Chain(Fn(minus)).Chain(Fn(mul)).Chain(Fn(divide))(10, 5)
    fmt.Println(result)
}Copy the code

Reference documentation

  1. En.wikipedia.org/wiki/Decora…
  2. Stackoverflow.com/questions/2…
  3. Docs.microsoft.com/en-us/azure…
  4. En.wikipedia.org/wiki/Chain-…
  5. Stackoverflow.com/questions/4…
  6. Github.com/alex-leonha…
  7. Coolshell. Cn/articles / 17…
  8. www.bartfokker.nl/posts/decor…
  9. Drstearns. Making. IO/tutorials/g…
  10. Preslav. Me / 2019/07/07 /…
  11. En.wikipedia.org/wiki/Pipeli…).
  12. Github.com/gin-gonic/g…
  13. Github.com/gin-gonic/g…
  14. Github.com/labstack/ec…
  15. Github.com/kataras/iri…