什么是拦截器
gRPC拦截器(interceptor)是个函数,它可以在gRPC执行服务之前和之后执行一些逻辑,例如认证、授权、日志记录、监控和统计等
函数作为实参传递
在 Go 中,一个普通的函数长这样:
func Service(request Request) (Response, error)
作用就是传入请求,完成处理后返回响应,有错误就返回 error
作为一门函数式编程语言,同样函数也支持把函数当实参传入另一个函数中,比如
type Handler func(request Request) (Response, error)
把最开始的函数签名定义成 Handler ,那么我们就可以这样来使用:
func WrapHandler(request Request, handler Handler) (Response, error) {
...... // pre
resp, err := handler(request)
...... // post
return resp, err
}
如何,相当于我们把实际需要实现的业务逻辑提取到外部定义,这样就可以在实际处理业务之前或者之后定义一些额外的逻辑。
拦截器原理
现在来抽象一下,把 Service 方法弄个称呼:业务方法 ,函数签名如下
func Service(ctx Context, req any) (any, error)
然后给这个签名定义一个类型
type Handler func(ctx Context, req any) (any, error)
记住,Handler 代表的就是一个最终的业务方法实现,现在把这个业务方法作为实参传递到另一个函数中去
func Intercept(ctx Context, req any, handler Handler) (any, error) {
...... // 前处理
resp, err := Handler(ctx, request) // 调用业务方法
...... // 后处理
return resp, err
}
完事,这就是拦截器的原理:在业务方法之前或者之后增加额外的逻辑,这就是某些支持注解的语言(Java、TS)的原理,或者把当作装饰设计模式看待。
拦截器链原理
再来抽象一下,把 Intercept 也弄个称呼:拦截器 ,函数签名如下
type Interceptor func(ctx Context, req any, handler Handler) (any, error)
记住,Interceptor 代表的就是一个拦截器,在调用业务方式时增加前处理和后处理逻辑。
那么问题来了,如果我定义了两个拦截器:
func Interceptor1(ctx Context, req any, handler Handler) (any, error) {
return nil, nil
}
func Interceptor2(ctx Context, req any, handler Handler) (any, error) {
return nil, nil
}
业务方法好像?似乎?也许?只能放到 拦截器 1 或者 拦截器 2 其中一个执行,然后返回响应。但是我想业务方法先流过拦截器 1,然后流过拦截器 2,这样就可以不断新增拦截器逻辑,而不必全堆在一个拦截器中实现全部额外逻辑。
第一版
那有没有办法实现这种逻辑呢?听我说,有的兄弟,有的。
func WrapInterceptors(ctx Context, req any, handler Handler) (any, err) {
interceptor1 := func(ctx Context, req any, handler Handler) (any, err) {
...... // 1 -> pre
interceptor2 := func(ctx Context, req any, handler Handler) (any, err) {
...... // 2 -> pre
resp, err := handler(ctx, req) // 业务方法
...... // 2 -> post
return resp, err
}
...... // 1 -> post
return interceptor2(ctx, req, handler)
}
return interceptor1(ctx, req, handler)
}
resp, err := WrapInterceptors(ctx, req, handler)
怎么样兄弟,满不满足要求?什么?你说这样的实现太丑陋了?要高级点?好吧,听我说,有的兄弟,有的。
第二版
先改动一下拦截器的函数签名,让拦截器返回业务处理函数,记住更改签名后,拦截器是包装业务函数的函数,返回也是业务函数
type Interceptor func(handler Handler) Handler
拦截器的形式改成闭包形式:
func Interceptor1(handler Handler) Handler {
return func(ctx Context, req any) (any, error) {
...... // pre-processing
resp, err := handler(ctx, req)
...... // post-processing
return response, err
}
}
多个拦截器装饰在一起就完事了
func WrapInterceptors(ctx Context, req any, handler Handler) (any, error) {
wrap := Interceptor1(Interceptor2(handler))
return wrap(ctx, req)
}
这也是Go里面HTTP的拦截器写法,如何兄弟,收不收货?什么?你说这样写还是很丑陋。好吧,要求真多,不过你听我说,有的兄弟,还是有的。
第三版
一些前缀知识
// 切片的最后一个元素
last := slice[len(slice)-1]
// 移除切片的最后一个元素
slice := slice[:len(slice)-1]
然后只能祭出 闭包 + 递归 大法了
func WrapInterceptors(interceptors ...Interceptor) Interceptor {
if len(interceptors) == 1 {
return interceptors[0]
}
return func(handler Handler) Handler {
last := len(interceptors) - 1
wrap = interceptors[last](handler) // 从最后一个拦截器开始包装业务方法
return WrapInterceptors(interceptors[:last]...)(wrap)
}
}
怎样兄弟?什么?你说倒序太难看懂,行吧,有的,顺序也有的
func WrapInterceptors(interceptors ...Interceptor) Interceptor {
if len(interceptors) == 1 {
return interceptors[0]
}
return func(handler Handler) Handler {
wrap := WrapInterceptors(interceptors[1:]...)(handler)
return interceptors[0](wrap)
}
}
func main() {
interceptor := WrapInterceptors(Interceptor1, Interceptor2)
resp, err := interceptor(handler)(ctx, req)
......
}
兄弟兄弟,怎么样,是时候收收货了吧?什么?你还想不改动拦截器的签名?特喵的,屁事还真多,行,没问题。
最终版
func WrapHandler(ctx Context, req any, handler Handler, interceptors ...Interceptor) Handler {
if len(interceptors) == 0 {
return handler
}
return func(ctx Context, req any) (any, error) {
wrap := WrapHandler(ctx, req, handler, interceptors[1:]...)
return interceptors[0](ctx, req, wrap)
}
}
多个拦截器直接传入即可
func main() {
fn := WrapInterceptors(ctx, req, handler, Interceptor1, Interceptor2)
resp, err := fn(ctx, req)
......
}
兄弟,夜里小心走路脚滑。
其实gRPC的拦截器就是以上的方式实现的,就是定义拦截器的时候多了几个形参而已,原理就是那么点事,下次聊聊gRPC的拦截器。