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

相关推荐
未秃头的程序猿21 分钟前
给公司做了个AI客服Agent,用的Spring AI 1.0,3天上线领导拍板了
java·后端·ai编程
Darren24530 分钟前
MySQL索引执行计划不走索引下推
后端
程序员清风35 分钟前
OpenAI官方发布最新提示词技巧!
java·后端·面试
码事漫谈1 小时前
人机协同的三重范式:HITL、HOTL与HOOTL
后端
武子康1 小时前
Inkling 975B 说明“开放权重“与“普通开发者本地运行“已经分离,内容重点应是部署容量和运行时边界
前端·人工智能·后端
神奇小汤圆1 小时前
Spring Boot请求处理组件对比详解
后端
颜酱2 小时前
03 | 实现节点1 — 抽取关键词
前端·人工智能·后端
qq_452396232 小时前
第五篇:《接口与错误处理:Go 的哲学》
开发语言·后端·golang
神奇小汤圆2 小时前
SpringBoot + 虚拟线程,简直鸟枪换大炮~
后端
掘金一周2 小时前
想问问掘友们:AI 时代,资深开发的核心价值到底在哪里?| 沸点周刊 7.23
前端·人工智能·后端