什么是拦截器

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