Go語言學習——如何實現一個過濾器

  • 2019 年 10 月 3 日
  • 筆記

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過濾器

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則繼續執行下一個過濾器。

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、重構實現

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中,再傳遞到下一個過濾器。

myContext := MyContext{Context: context.TODO(), KeyValue: map[string]bool{"key": false}}

上面的等價寫法還可以是

ctx := context.TODO()  myContext := context.WithValue(ctx, "key", "value")

這裡充分利用了Context的WithValue的用法,有興趣可以去看下,這是Context創建map鍵值對的方式。

 

4.2 充分利用Go的type的特性

 

type FilterFunc func(*MyContext) bool

 

  前面在使用過濾的場景特種中提到,過濾器的入參和返回值都是一樣的。所以這裡我們利用Go的type特性,將這種過濾器函數定義為一個變量FilterFunc

  這一特性對於精簡代碼起到了關鍵性的作用。且看

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…

 

type FilterFuncChain []FilterFunc

  這裡的抽象也是同樣的道理。

  如果之前寫過Java,這裡是不是已經看到了Filter接口的影子。其實這裡的FilterFunc可以等價於Java裏面的Filter接口,接口是一種約束一種契約,Filter定義了如果要實現該接口必須要實現接口定義的方法。

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 遍歷執行過濾器

  因為有了上面的特性,我們才能將這些過濾器存入切片然後依次執行,如下

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函數的返回值,改進後如下

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的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。