凡秀 發表於 2019-8-30 00:58:00

Go语言学习——如何实现一个过滤器

<p><span style="color: rgba(51, 102, 255, 1)"><strong><span style="font-size: 18pt; font-family: &quot;Microsoft YaHei&quot;">1、过滤器使用场景</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  做业务的时候我们经常要使用过滤器或者拦截器(听这口音就是从Java过来的)。常见的场景如一个HTTP请求,需要经过鉴权过滤器、白名单校验过滤、参数验证过滤器等重重关卡最终拿到数据。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  Java使用过滤器很简单。XML时代,只要添加一个过滤器配置再新建一个实现了Filter接口的xxxFilter实现类;Java Configuration时代,只要在xxxConfiguration配置类中声明一个Filter注解,如果想设置Filter的执行顺序,加上Order注解就行了。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  Java的过滤器实在太方便也太好用了。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  以至于在Java有关过滤器的面试题中,只有类似于“过滤器的使用场景有哪些?”,“过滤器和拦截器有什么区别?“,几乎很少听到”你知道过滤器是怎么实现的吗?“,”如果让你实现一个过滤器,你会怎么做?“这样的题目。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="color: rgba(51, 102, 255, 1)"><strong><span style="font-size: 18pt; font-family: &quot;Microsoft YaHei&quot;">2、使用过滤器的场景特征</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">如同上面过滤器的例子,我们发现过滤器有一些特征:</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  1、入参一样,比如HTTP请求的过滤器的入参就是ServletRequest对象</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  2、返回值类型相同,比如都是true或者false,或者是链接到下一个过滤器或者return。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">如下是Java实现的CORS过滤器</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CORSFilter implements Filter {

    @Override
    public void doFilter(ServletRequest reserRealmq, ServletResponse res, FilterChain chain) throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) reserRealmq;
      HttpServletResponse response = (HttpServletResponse) res;

      String currentOrigin= request.getHeader("Origin");
      if (!StringUtils.isEmpty(currentOrigin)) {
            response.setHeader("Access-Control-Allow-Origin", currentOrigin);
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Cache-Control, Expires, Content-Type, X-E4M-With, Index-Url");
      }

      // return http status 204 if OPTIONS requst
      if ("OPTIONS".equals(request.getMethod())){
            response.setStatus(HttpStatus.NO_CONTENT.value());
      }else {
            chain.doFilter(reserRealmq, res);
      }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }
}
</pre>
</div>
<p>  </p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">凡是具有这种特征的需求,我们都可以抽象为过滤器进行实现(Java里面称为责任链模式)。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">下面就来说说,基于Go语言如何实现一个过滤器。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="font-size: 18pt; color: rgba(51, 102, 255, 1)"><strong><span style="font-family: &quot;Microsoft YaHei&quot;">3、简单实现</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  过滤器本质就是一堆条件判定,最直观的过滤方案就是创建几个方法,针对每个方法的返回结果判定,如果返回为false则终止请求,如果为true则继续执行下一个过滤器。</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">package main

import (
        "context"
)

func main() {
        ctx := context.TODO()

if continued := F1(ctx); !continued {
    ...
    return
}

if continued := F2(ctx); !continued {
    ...
    return
}

if continued := F3(ctx); !continued {
    ...
    return
}
}

func F1(ctx context.Context) bool {
...
return true
}

func F2(ctx context.Context) bool {
...
return true
}

func F3(ctx context.Context) bool {
...
return false
}
</pre>
</div>
<p>  </p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">该版本从功能上说,完全符合过滤器的要求。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">但是从代码层面来说,有几个问题:</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  1、复用性较差。main函数中对于各个过滤器的判定,除了函数名不一样,其他逻辑都一样,可以考虑抽象重用。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  2、可扩展性较差。因为有些代码复用性差,导致代码不好扩展,如果这时候添加、删除过滤器或者调整过滤器执行顺序,代码都需要较大改动才能实现。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  3、难以维护。不用多说。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="color: rgba(51, 102, 255, 1)"><strong><span style="font-size: 18pt; font-family: &quot;Microsoft YaHei&quot;">4、重构实现</span></strong></span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">package main

import (
        "context"
        "fmt"
)

type MyContext struct {
        context.Context
        KeyValue mapbool
}

type FilterFunc func(*MyContext) bool

type FilterFuncChain []FilterFunc

type CombinedFunc struct {
        CF    FilterFuncChain
        MyCtx *MyContext
}

func main() {
        myContext := MyContext{Context: context.TODO(), KeyValue: mapbool{"key": false}}

        cf := CombinedFilter(&amp;myContext, F1, F2, F3);
        DoFilter(cf)
}

func DoFilter(cf *CombinedFunc) {
        for _, f := range cf.CF {
                res := f(cf.MyCtx)
                fmt.Println("result:", res)
                if res == false {
                        fmt.Println("stopped")
                        return
                }
        }
}

func CombinedFilter(ctx *MyContext, ff ...FilterFunc) *CombinedFunc {
        return &amp;CombinedFunc{
                CF:    ff,
                MyCtx: ctx,
        }
}

func F1(ctx *MyContext) bool {
        ctx.KeyValue["key"] = true
        fmt.Println(ctx.KeyValue["key"])

        return ctx.KeyValue["key"]
}

func F2(ctx *MyContext) bool {
        ctx.KeyValue["key"] = false
        fmt.Println(ctx.KeyValue["key"])

        return ctx.KeyValue["key"]
}

func F3(ctx *MyContext) bool {
        ctx.KeyValue["key"] = false
        fmt.Println(ctx.KeyValue["key"])

        return ctx.KeyValue["key"]
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">代码不长,我们一块块分析。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">4.1 自定义的Context</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  这里我使用了自定义的Context,重新定义一个MyContext的结构体,其中组合了标准库中的Context,即具备标准库Context的能力。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  这里MyContext是作为数据载体在各个过滤器之间传递。没有用标准库的Context,采用自定义的Context主要是为了说明我们可以根据需要扩展MyContext,通过扩展MyContext添加任何我们需要的参数。这里添加的是一个map键值对。我们可以将每个过滤器处理的结果存入这个map中,再传递到下一个过滤器。</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">myContext := MyContext{Context: context.TODO(), KeyValue: mapbool{"key": false}}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">上面的等价写法还可以是</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">ctx := context.TODO()
myContext := context.WithValue(ctx, "key", "value")</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">这里充分利用了Context的WithValue的用法,有兴趣可以去看下,这是Context创建map键值对的方式。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">4.2 充分利用Go的type的特性</span></strong></p>
<p>&nbsp;</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">type FilterFunc func(*MyContext) bool</pre>
</div>
<p>&nbsp;</p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  前面在使用过滤的场景特种中提到,过滤器的入参和返回值都是一样的。所以这里我们利用Go的type特性,将这种过滤器函数定义为一个变量FilterFunc</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  这一特性对于精简代码起到了关键性的作用。且看</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">cf := CombinedFilter(&amp;myContext, F1, F2, F3);

func CombinedFilter(ctx *MyContext, ff ...FilterFunc) *CombinedFunc {
        return &amp;CombinedFunc{
                CF:    ff,
                MyCtx: ctx,
        }
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">因为这里的F1、F2和F3都有相同入参和返回值,所以抽象为FilterFunc,并使用变长参数的FilterFunc统一接收。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">CombinedFilter不仅可以加F1、F2和F3,后面还可以有F4、F5...</span></p>
<p>&nbsp;</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">type FilterFuncChain []FilterFunc</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  这里的抽象也是同样的道理。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  如果之前写过Java,这里是不是已经看到了Filter接口的影子。其实这里的FilterFunc可以等价于Java里面的Filter接口,接口是一种约束一种契约,Filter定义了如果要实现该接口必须要实现接口定义的方法。</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:java;gutter:true;">package javax.servlet;

import java.io.IOException;

/**
* A FilterChain is an object provided by the servlet container to the developer
* giving a view into the invocation chain of a filtered request for a resource.
* Filters use the FilterChain to invoke the next filter in the chain, or if the
* calling filter is the last filter in the chain, to invoke the resource at the
* end of the chain.
*
* @see Filter
* @since Servlet 2.3
**/

public interface FilterChain {

    /**
   * Causes the next filter in the chain to be invoked, or if the calling
   * filter is the last filter in the chain, causes the resource at the end of
   * the chain to be invoked.
   *
   * @param request
   *            the request to pass along the chain.
   * @param response
   *            the response to pass along the chain.
   *
   * @throws IOException if an I/O error occurs during the processing of the
   *                     request
   * @throws ServletException if the processing fails for any other reason

   * @since 2.3
   */
    public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException;

}
</pre>
</div>
<p>  </p>
<p><strong><span style="font-size: 14pt; font-family: &quot;Microsoft YaHei&quot;">4.3 遍历执行过滤器</span></strong></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">  因为有了上面的特性,我们才能将这些过滤器存入切片然后依次执行,如下</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">func DoFilter(cf *CombinedFunc) {
        for _, f := range cf.CF {
                res := f(cf.MyCtx)
                fmt.Println("result:", res)
                if res == false {
                        fmt.Println("stopped")
                        return
                }
        }
}</pre>
</div>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">在执行的过程中,如果我们发现如果返回值为false,则表示没有通过某个过滤器校验,则退出也不会继续执行后面的过滤器。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p><span style="color: rgba(51, 102, 255, 1)"><strong><span style="font-size: 18pt; font-family: &quot;Microsoft YaHei&quot;">5、继续改进</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">既然MyContext中的map集合可以存储各个Filter的执行情况,而且可以在各个过滤器之间传递,我们甚至可以省略FilterFunc函数的返回值,改进后如下</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">package main

import (
        "context"
        "fmt"
)

type MyContext struct {
        context.Context
        KeyValue mapbool
}

type FilterFunc func(*MyContext)

type FilterFuncChain []FilterFunc

type CombinedFunc struct {
        CF    FilterFuncChain
        MyCtx *MyContext
}

func main() {
        myContext := MyContext{Context: context.TODO(), KeyValue: mapbool{"key": false}}

        cf := CombinedFilter(&amp;myContext, F1, F2, F3);
        DoFilter(cf)
}

func DoFilter(cf *CombinedFunc) {
        for _, f := range cf.CF {
                f(cf.MyCtx)
                continued :=cf.MyCtx.KeyValue["key"]
                fmt.Println("result:", continued)
                if !continued {
                        fmt.Println("stopped")
                        return
                }
        }
}

func CombinedFilter(ctx *MyContext, ff ...FilterFunc) *CombinedFunc {
        return &amp;CombinedFunc{
                CF:    ff,
                MyCtx: ctx,
        }
}

func F1(ctx *MyContext) {
        ctx.KeyValue["key"] = true
        fmt.Println(ctx.KeyValue["key"])
        //return ctx.KeyValue["key"]
}

func F2(ctx *MyContext) {
        ctx.KeyValue["key"] = false
        fmt.Println(ctx.KeyValue["key"])
        //return ctx.KeyValue["key"]
}

func F3(ctx *MyContext) {
        ctx.KeyValue["key"] = false
        fmt.Println(ctx.KeyValue["key"])
        //return ctx.KeyValue["key"]
}
</pre>
</div>
<p>  </p>
<p><span style="font-size: 18pt; color: rgba(51, 102, 255, 1)"><strong><span style="font-family: &quot;Microsoft YaHei&quot;">6、总结</span></strong></span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">基于Go语言造轮子实现一个过滤器的雏形,通过实现一个相对优雅可扩展的过滤器熟悉了type的用法,Context.WithValue的作用。</span></p>
<p><span style="font-size: 16px; font-family: &quot;Microsoft YaHei&quot;">&nbsp;</span></p>
<p>如果您觉得阅读本文对您有帮助,请点一下“<strong>推荐</strong>”按钮,您的<strong>“推荐”</strong>将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。</p>
<pre><em><img src="https://images0.cnblogs.com/blog2015/619240/201505/162205410643708.jpg" alt=""></em></pre><br><br>
来源:https://www.cnblogs.com/bigdataZJ/p/go-filter.html
頁: [1]
查看完整版本: Go语言学习——如何实现一个过滤器