Go语言学习——如何实现一个过滤器
1、过滤器使用场景
做业务的时候我们经常要使用过滤器或者拦截器(听这口音就是从Java过来的)。常见的场景如一个HTTP请求,需要经过鉴权过滤器、白名单校验过滤、参数验证过滤器等重重关卡最终拿到数据。
Java使用过滤器很简单。XML时代,只要添加一个过滤器配置再新建一个实现了Filter接口的xxxFilter实现类;Java Configuration时代,只要在xxxConfiguration配置类中声明一个Filter注解,如果想设置Filter的执行顺序,加上Order注解就行了。
Java的过滤器实在太方便也太好用了。
以至于在Java有关过滤器的面试题中,只有类似于“过滤器的使用场景有哪些?”,“过滤器和拦截器有什么区别?“,几乎很少听到”你知道过滤器是怎么实现的吗?“,”如果让你实现一个过滤器,你会怎么做?“这样的题目。
2、使用过滤器的场景特征
如同上面过滤器的例子,我们发现过滤器有一些特征:
1、入参一样,比如HTTP请求的过滤器的入参就是ServletRequest对象
2、返回值类型相同,比如都是true或者false,或者是链接到下一个过滤器或者return。
如下是Java实现的CORS过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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() { } } |
凡是具有这种特征的需求,我们都可以抽象为过滤器进行实现(Java里面称为责任链模式)。
下面就来说说,基于Go语言如何实现一个过滤器。
3、简单实现
过滤器本质就是一堆条件判定,最直观的过滤方案就是创建几个方法,针对每个方法的返回结果判定,如果返回为false则终止请求,如果为true则继续执行下一个过滤器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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 } |
该版本从功能上说,完全符合过滤器的要求。
但是从代码层面来说,有几个问题:
1、复用性较差。main函数中对于各个过滤器的判定,除了函数名不一样,其他逻辑都一样,可以考虑抽象重用。
2、可扩展性较差。因为有些代码复用性差,导致代码不好扩展,如果这时候添加、删除过滤器或者调整过滤器执行顺序,代码都需要较大改动才能实现。
3、难以维护。不用多说。
4、重构实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | package main import ( "context" "fmt" ) type MyContext struct { context.Context KeyValue map [string]bool } type FilterFunc func (*MyContext) bool type FilterFuncChain []FilterFunc type CombinedFunc struct { CF FilterFuncChain MyCtx *MyContext } func main() { myContext := MyContext{Context: context.TODO(), KeyValue: map [string]bool{ "key" : false}} cf := CombinedFilter(&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 &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" ] } |
代码不长,我们一块块分析。
4.1 自定义的Context
这里我使用了自定义的Context,重新定义一个MyContext的结构体,其中组合了标准库中的Context,即具备标准库Context的能力。
这里MyContext是作为数据载体在各个过滤器之间传递。没有用标准库的Context,采用自定义的Context主要是为了说明我们可以根据需要扩展MyContext,通过扩展MyContext添加任何我们需要的参数。这里添加的是一个map键值对。我们可以将每个过滤器处理的结果存入这个map中,再传递到下一个过滤器。
1 | myContext := MyContext{Context: context.TODO(), KeyValue: map [string]bool{ "key" : false}} |
上面的等价写法还可以是
1 2 | ctx := context.TODO() myContext := context.WithValue(ctx, "key" , "value" ) |
这里充分利用了Context的WithValue的用法,有兴趣可以去看下,这是Context创建map键值对的方式。
4.2 充分利用Go的type的特性
1 | type FilterFunc func (*MyContext) bool |
前面在使用过滤的场景特种中提到,过滤器的入参和返回值都是一样的。所以这里我们利用Go的type特性,将这种过滤器函数定义为一个变量FilterFunc
这一特性对于精简代码起到了关键性的作用。且看
1 2 3 4 5 6 7 8 | cf := CombinedFilter(&myContext, F1, F2, F3); func CombinedFilter(ctx *MyContext, ff ...FilterFunc) *CombinedFunc { return &CombinedFunc{ CF: ff, MyCtx: ctx, } } |
因为这里的F1、F2和F3都有相同入参和返回值,所以抽象为FilterFunc,并使用变长参数的FilterFunc统一接收。
CombinedFilter不仅可以加F1、F2和F3,后面还可以有F4、F5...
1 | type FilterFuncChain []FilterFunc |
这里的抽象也是同样的道理。
如果之前写过Java,这里是不是已经看到了Filter接口的影子。其实这里的FilterFunc可以等价于Java里面的Filter接口,接口是一种约束一种契约,Filter定义了如果要实现该接口必须要实现接口定义的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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; } |
4.3 遍历执行过滤器
因为有了上面的特性,我们才能将这些过滤器存入切片然后依次执行,如下
1 2 3 4 5 6 7 8 9 10 | func DoFilter(cf *CombinedFunc) { for _, f := range cf.CF { res := f(cf.MyCtx) fmt.Println( "result:" , res) if res == false { fmt.Println( "stopped" ) return } } } |
在执行的过程中,如果我们发现如果返回值为false,则表示没有通过某个过滤器校验,则退出也不会继续执行后面的过滤器。
5、继续改进
既然MyContext中的map集合可以存储各个Filter的执行情况,而且可以在各个过滤器之间传递,我们甚至可以省略FilterFunc函数的返回值,改进后如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | package main import ( "context" "fmt" ) type MyContext struct { context.Context KeyValue map [string]bool } type FilterFunc func (*MyContext) type FilterFuncChain []FilterFunc type CombinedFunc struct { CF FilterFuncChain MyCtx *MyContext } func main() { myContext := MyContext{Context: context.TODO(), KeyValue: map [string]bool{ "key" : false}} cf := CombinedFilter(&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 &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"] } |
6、总结
基于Go语言造轮子实现一个过滤器的雏形,通过实现一个相对优雅可扩展的过滤器熟悉了type的用法,Context.WithValue的作用。
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 后端思维之高并发处理方案
· 理解Rust引用及其生命周期标识(下)
· 从二进制到误差:逐行拆解C语言浮点运算中的4008175468544之谜
· .NET制作智能桌面机器人:结合BotSharp智能体框架开发语音交互
· 软件产品开发中常见的10个问题及处理方法
· 2025成都.NET开发者Connect圆满结束
· 后端思维之高并发处理方案
· 千万级大表的优化技巧
· 在 VS Code 中,一键安装 MCP Server!
· 10年+ .NET Coder 心语 ── 继承的思维:从思维模式到架构设计的深度解析
2017-08-30 Spring集成RabbitMQ-必须知道的几个概念