gRPC拦截器(一)

什么是拦截器

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的拦截器。

相关推荐
晚风吹长发1 小时前
初步了解Linux中的动静态库及其制作和使用
linux·运维·服务器·数据结构·c++·后端·算法
梁下轻语的秋缘2 小时前
ESP32-WROOM-32E存储全解析:RAM/Flash/SD卡读写与速度对比
java·后端·spring
wanzhong23332 小时前
开发日记8-优化接口使其更规范
java·后端·springboot
羊小猪~~4 小时前
【QT】--文件操作
前端·数据库·c++·后端·qt·qt6.3
宇宙帅猴4 小时前
【Ubuntu踩坑及解决方案(一)】
linux·运维·ubuntu·go
张彦峰ZYF4 小时前
商品供给域的工程化简要设计考量
后端·系统架构·商品模型·商品供给
小北方城市网5 小时前
微服务注册中心与配置中心实战(Nacos 版):实现服务治理与配置统一
人工智能·后端·安全·职场和发展·wpf·restful
爬山算法6 小时前
Hibernate(47)Hibernate的会话范围(Scope)如何控制?
java·后端·hibernate
源码宝8 小时前
云HIS二次开发实施路径指南
后端·源码·二次开发·saas·云his·医院信息系统
李慕婉学姐10 小时前
Springboot旅游景点管理系统2fj40iq6(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端