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

相关推荐
Ysx2 分钟前
踩了 5 次"本地全绿、生产失效"之后,我把交付流程做成了 SOP
后端·程序员
pippocao7 分钟前
王者荣耀日志组件BqLog为什么这么快之2——从环形队列到自适应数据总线
后端
孟健22 分钟前
Stripe出海收款架构设计:水星银行与香港账户实测对比与资金流闭环
后端·架构
程序员cxuan38 分钟前
DeepSeek Harness 出了桌面端?我把它扒了一遍
人工智能·后端·程序员
j7~40 分钟前
【Python】(篇七)《使用Python库》 -- 详解
开发语言·后端·python·编程学习·python标准库·python第三方库·python拓展
Memory_荒年1 小时前
订单超时未支付?从“定时扫库”一步步到最终形态
java·后端
卷无止境2 小时前
便宜模型和贵模型到底差在哪儿?一份关于Claude与GPT分级体系的深度梳理
后端·python
阿狗童鞋2 小时前
SpringBoot微服务实战指南
spring boot·后端·微服务
FfHUCisI2 小时前
sync.Once 与 sync.Cond 源码与并发控制陷阱
服务器·开发语言·后端·golang
montEvergreen2 小时前
位运算不是魔法——AV1 配置解析实战
后端