Filter (also known as middleware) is a very extensive framework component used in our usual business. Many web frameworks and microservice frameworks have integrations. Usually before and after some requests, we will put the more general logic in the filter component to implement. General logic such as request log, time-consuming, permission, interface current limit, etc. So next I will implement a filter component with you, and let you know how it was built from 0 to 1, what problems were encountered in the evolution process, and how to solve it.

Speaking from a simple server

Let's look at such a piece of code. First, we open an http server on the server, configure the / route, the hello function processes the request of this route, and writes the hello string response to the client in the body. We can see the response result by visiting 127.0.0.1:8080. The specific implementation is as follows:

// 模拟业务代码
func hello(wr http.ResponseWriter, r *http.Request) {
    wr.Write([]byte("hello"))
}

func main() {
    http.HandleFunc("/", hello)
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

Print request time-consuming v1.0

Next, there is a requirement to print the execution time of this request. This is also a common scenario in our business. We might implement it like this, adding time calculation logic to the hello handler method, and the main function remains unchanged:

// 模拟业务代码
func hello(wr http.ResponseWriter, r *http.Request) {
    // 增加计算执行时间逻辑
    start := time.Now()
    wr.Write([]byte("hello"))
    timeElapsed := time.Since(start)
    // 打印请求耗时
    fmt.Println(timeElapsed)
}

func main() {
    http.HandleFunc("/", hello)
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

But this implementation still has certain problems. Suppose we have ten thousand request path definitions, so ten thousand handlers correspond to it. If we add the calculation of request execution time to these ten thousand handlers, the price will be considerable.

In order to improve the code reuse rate, we use the filter component to solve such problems. Most web frameworks or microservice frameworks provide this component, which is also called middleware in some frameworks.

filter debut

The basic idea of filter is to separate functional (business code) from non-functional (non-business code) to ensure no intrusion into business code and improve code reusability. Before explaining the realization of the requirements of 2.0, let's review the more important function calls in 1.0 http.HandleFunc("/", hello)

This function will receive a routing rule pattern, and the processing function handler corresponding to this routing. Our general business logic will be written in the handler, here is the hello function. Let's look at the detailed definition of the http.HandleFunc() function next:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 

It should be noted here that the func (ResponseWriter, *Request) in the standard library redefines the func as a type alias HandlerFunc:

type HandlerFunc func(ResponseWriter, *Request)

So the http.HandleFunc() function definition we used at the beginning can be directly simplified to this:

func HandleFunc(pattern string, handler HandlerFunc) 

We only need to distinguish the "HandlerFunc type" from the "HandleFunc function" to make it clear at a glance. Because the user function hello also conforms to the definition of the HandlerFunc type, it can naturally be passed directly to the http.HandlerFunc function. The HandlerFunc type is actually an implementation of the Handler interface. The implementation of the Handler interface is as follows. It has only one method, ServeHTTP:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

HandlerFunc is the default Handler interface implementation provided in the standard library, so it must implement the ServeHTTP method. It only does one thing in ServeHTTP, which is to call the handler passed in by the user to execute specific business logic. In our case, it executes hello(), prints the string, and the entire request response process ends.

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

Print request time-consuming v2.0

So the easier way we can think of is to wrap the incoming user business function hello on the outside instead of adding the code for printing time in hello. We can define a timeFilter function separately, he receives a parameter f, also http.HandlerFunc type, and then add the time.Now, time.Since code before and after the f we passed in.

Note here that the final return value of timeFilter is also a http.HandlerFunc function type, because after all, it is ultimately passed to the http.HandleFunc function, so the filter must also return this type, so that the final business code and non-business code can be separated At the same time, the print request time is realized. The detailed implementation is as follows:

// 打印请求时间filter,和具体的业务逻辑hello解耦
func timeFilter(f http.HandlerFunc) http.HandlerFunc {
    return func(wr http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // 这里就是上面我们看过HandlerFun类型中ServeHTTP的默认实现,会直接调用f()执行业务逻辑,这里就是我们的hello,最终会打印出字符串
        f.ServeHTTP(wr, r)
        timeElapsed := time.Since(start)
        // 打印请求耗时
        fmt.Println(timeElapsed)
    }
}

func hello(wr http.ResponseWriter, r *http.Request) {
    wr.Write([]byte("hello\n"))
}

func main() {
    // 在hello的外面包上一层timeFilter
    http.HandleFunc("/", timeFilter(hello))
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

However, there are still two problems with this:

  • If there are 100,000 routes, should I add the same package code to each of these 100,000 routes?
  • If there are one hundred thousand filters, do we have to wrap one hundred thousand layers? The code readability will be very poor

The current implementation is likely to cause the following consequences:

http.HandleFunc("/123", filter3(filter2(filter1(hello))))
http.HandleFunc("/456", filter3(filter2(filter1(hello))))
http.HandleFunc("/789", filter3(filter2(filter1(hello))))
http.HandleFunc("/135", filter3(filter2(filter1(hello))))
...

So how to manage the relationship between filters and routes more elegantly, so that filter3(filter2(filter1(hello))) can be written once to apply to all routes?

Print request time-consuming v3.0

We can imagine that we first extract the definition of the filter and define it as the Filter type separately, and then define a structure Frame with the filters field used to manage all filters. This can be seen from the main function. We added timeFilter, routing, and finally opened the service, which is roughly the same as the 1.0 version of the process:

// Filter类型定义
type Filter func(f http.HandlerFunc) http.HandlerFunc

type Frame struct {
    // 存储所有注册的过滤器
    filters []Filter
}

// AddFilter 注册filter
func  (r *Frame) AddFilter(filter Filter) {
    r.filters = append(r.filters, filter)
}

// AddRoute 注册路由,并把handler按filter添加顺序包起来。这里采用了递归实现比较好理解,后面会讲迭代实现
func (r *Frame) AddRoute(pattern string, f http.HandlerFunc) {
    r.process(pattern, f, len(r.filters) - 1)
}

func (r *Frame) process(pattern string, f http.HandlerFunc, index int) {
    if index == -1 {
        http.HandleFunc(pattern, f)
        return
    }
    fWrap := r.filters[index](f)
    index--
    r.process(pattern, fWrap, index)
}

// Start 框架启动
func (r *Frame) Start() {
    if err := http.ListenAndServe(":8080", nil); err != nil {
        panic(err)
    }
}

func main() {
    r := &Frame{}
    r.AddFilter(timeFilter)
    r.AddFilter(logFilter)
    r.AddRoute("/", hello)
    r.Start()
}

r.AddRoute is well understood before, initialize the main structure, and put our defined filter into the slice of the main structure for unified management. Next, AddRoute here is the core logic, and then we will explain in detail

AddRoute

r.AddRoute("/", hello) is actually exactly the same as http.HandleFunc("/", hello) in v1.0, except that the filter logic is added internally. The process function will be called inside r.AddRoute, and I will replace all the parameters with specific values:

r.process("/", hello, 1)

Then in the process, first the index is not equal to -1, and then execute to

fWrap := r.filters[index](f)

His meaning is to take out the index filter, which is currently r.filters[1], r.filters[1] is our logFilter, logFilter receives an f (here is hello), and f.ServerHTTP in logFilter can be viewed directly To execute f(), or hello, is equivalent to directly replacing the f.ServerHTTP line in logFilter with the logic in hello, which is represented by an arrow in the figure below. Finally, assign the return value of logFilter to fWrap, and continue to recurse the wrapped fWrap down, index--:

In the same way, the following recursive parameters are:

r.process("/", hello, 0)

Here is the turn of r.filters[0], namely timeFilter, the process is the same as above:

In the last round of recursion, index = -1, that is, all filters have been processed, we can finally call http.HandleFunc(pattern, f) just like v1.0 to finally register the f that we have wrapped in layers. The whole process ends:

The recursive version of AddRoute is relatively easy to understand, and I also implemented a version using iteration. Each loop will re-assign f to f after wrapping f in this layer of filter, so that the f after the previous wrapping can be used in the next iteration, and continue to wrap the remaining filters based on the f in the previous round. In the gin framework, iterative method is used to achieve:

// AddRouteIter AddRoute的迭代实现
func (r *Frame) AddRouteIter(pattern string, f http.HandlerFunc) {
    filtersLen := len(r.filters)
    for i := filtersLen; i >= 0; i-- {
        f = r.filters[i](f)
    }
    http.HandleFunc(pattern, f)
}

The implementation of this filter is also called the onion mode. The innermost layer is our business logic helllo, then logFilter on the outside, and timeFilter on the outside, which is very similar to this onion. I believe you can already experience it here:

summary

From the very beginning, the business logic and non-business logic of version 1.0 were severely coupled. The filter was introduced in version 2.0 but the implementation was still not elegant, and version 3.0 solved the remaining problems of version 2.0, and finally implemented a simple filter management framework.

Follow us

Readers who are interested in this series of articles are welcome to subscribe to our official account, and pay attention to the blogger not to get lost next time~
image.png


NoSay
449 声望544 粉丝