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

過濾器使用場景

作業務的時候咱們常常要使用過濾器或者攔截器(聽這口音就是從Java過來的)。常見的場景如一個HTTP請求,須要通過鑑權過濾器、白名單校驗過濾、參數驗證過濾器等重重關卡最終拿到數據。java

Java使用過濾器很簡單。XML時代,只要添加一個過濾器配置再新建一個實現了Filter接口的xxxFilter實現類;Java Configuration時代,只要在xxxConfiguration配置類中聲明一個Filter註解,若是想設置Filter的執行順序,加上Order註解就好了。面試

Java的過濾器實在太方便也太好用了。spring

以致於在Java有關過濾器的面試題中,只有相似於「過濾器的使用場景有哪些?」,「過濾器和攔截器有什麼區別?「,幾乎不多聽到」你知道過濾器是怎麼實現的嗎?「,」若是讓你實現一個過濾器,你會怎麼作?「這樣的題目。markdown

使用過濾器的場景特徵

如同上面過濾器的例子,咱們發現過濾器有一些特徵:ide

一、入參同樣,好比HTTP請求的過濾器的入參就是ServletRequest對象函數

二、返回值類型相同,好比都是true或者false,或者是連接到下一個過濾器或者return。spa

以下是Java實現的CORS過濾器code

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裏面稱爲責任鏈模式)。orm

下面就來講說,基於Go語言如何實現一個過濾器。對象

簡單實現

過濾器本質就是一堆條件斷定,最直觀的過濾方案就是建立幾個方法,針對每一個方法的返回結果斷定,若是返回爲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
}
複製代碼

該版本從功能上說,徹底符合過濾器的要求。

可是從代碼層面來講,有幾個問題:

一、複用性較差。main函數中對於各個過濾器的斷定,除了函數名不同,其餘邏輯都同樣,能夠考慮抽象重用。

二、可擴展性較差。由於有些代碼複用性差,致使代碼很差擴展,若是這時候添加、刪除過濾器或者調整過濾器執行順序,代碼都須要較大改動才能實現。

三、難以維護。不用多說。

重構實現

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"]
}

複製代碼

代碼不長,咱們一塊塊分析。

自定義的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鍵值對的方式。

充分利用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,
	}
}
複製代碼

由於這裏的F一、F2和F3都有相同入參和返回值,因此抽象爲FilterFunc,並使用變長參數的FilterFunc統一接收。

CombinedFilter不只能夠加F一、F2和F3,後面還能夠有F四、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;

}
複製代碼

遍歷執行過濾器

由於有了上面的特性,咱們才能將這些過濾器存入切片而後依次執行,以下

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,則表示沒有經過某個過濾器校驗,則退出也不會繼續執行後面的過濾器。

繼續改進

既然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"]
}

複製代碼

總結

基於Go語言造輪子實現一個過濾器的雛形,經過實現一個相對優雅可擴展的過濾器熟悉了type的用法,Context.WithValue的做用。

相關文章
相關標籤/搜索