编写中间件和错误处理。
中间件是一种位于请求处理链中的组件,用来在请求到达业务逻辑前、或响应返回客户端之前,进行一些通用的处理。常用的:比如参数校验,限流,日志记录等等。
在将kratos中间件之前,先回顾一下gin框架的中间件。
gin 采用了 “洋葱模型”(Onion Model)的方式执行中间件,使其能够以链式调用的方式拦截、修改或终止请求。

请求 -> 中间件1 -> 中间件2 -> 业务逻辑 -> 中间件2 -> 中间件1 -> 响应
gin框架涉及中间件相关有4个常用的方法,它们分别是c.Next()、c.Abort()、c.Set()、c.Get()。
从gin.default来看中间件的使用
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默认注册的两个中间件
return engine
}
再来看看Use里的函数,可以看到他接收了一系列中间件函数
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...) // 实际上还是调用的RouterGroup的Use函数
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers) // 将处理请求的函数与中间件函数结合
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
const abortIndex int8 = math.MaxInt8 / 2
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) { // 这里有一个最大限制
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
而中间件是通过c.Next()、c.Abort()来控制调用链的。
可以看到Next 通过判断hanler的长度,按顺序执行在handler里注册的函数。而Abort 通过将索引值设成最大,直接退出循环
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
func (c *Context) Abort() {
c.index = abortIndex // 直接将索引置为最大限制值,从而退出循环
}
c.Set()和c.Get()这两个方法多用于在多个函数之间通过c传递数据的,比如我们可以在认证中间件中获取当前请求的相关信息(userID等)通过c.Set()存入c,然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户。c就像是一根绳子,将该次请求相关的所有的函数都串起来了。
Kratos中间件
kratos也同gin框架一般,采用洋葱状并且内置了很多中间件的方法,可以直接使用。
可以通过实现 Middleware 接口,开发自定义 middleware,进行通用的业务处理,比如用户登录鉴权等。
接着来对比一下两者在中间件上在路由上的应用。
gin
全局中间件
router := gin.Default()
router.Use(gin.Logger(), gin.Recovery())
组路由中间件
v1 := router.Group("/v1")
v1.Use(VersionMiddleware())
v1.GET("/users", ...)
特定路由中间件
router.GET("/endpoint", AuthMiddleware(), controllerHandler)
Kratos
全局中间件
// http
// 定义opts
var opts = []http.ServerOption{
http.Middleware(
recovery.Recovery(), // 把middleware按照需要的顺序加入
tracing.Server(),
logging.Server(),
),
}
特定路由,kratos不同于gin,他是通过路由匹配的方式来对进行中间件的使用
http.Middleware(
selector.Server(recovery.Recovery(), tracing.Server(),testMiddleware).
Path("/hello.Update/UpdateUser", "/hello.kratos/SayHello").
Regex(`/test.hello/Get[0-9]+`).
Prefix("/kratos.", "/go-kratos.", "/helloworld.Greeter/").
Build(),
)
匹配规则(多参数):
Path(path...): 路由匹配
Regex(regex...): 正则匹配
Prefix(prefix...): 前缀匹配
Match(fn): 函数匹配,函数格式为func(ctx context.Context,operation string) bool。 operation为 path,函数返回值为true,匹配成功,ctx可使用transport.FromServerContext(ctx)或者 transport.FromClientContext(ctx获取 Transporter)。
kratos不同于gin,他通过来protobuf生成文件控制配置参数。
具体可查validate
最后在server包里添加中间件即可
httpSrv := http.NewServer(
http.Address(":8000"),
http.Middleware(
validate.Validator(),
))
grpcSrv := grpc.NewServer(
grpc.Address(":9000"),
grpc.Middleware(
validate.Validator(),
))
错误处理
当希望对不同的请求响应不同的错误就需要定义不同的错误响应,kratos的错误处理可以分为三步:
- 定义proto⽂件
- ⽣成代码
- 业务代码中使⽤⽣成的代码返回错误
import "errors/errors.proto";
// 多语言特定包名,用于源代码引用
option go_package = "review-service/api/review/v1;v1";
option java_multiple_files = true;
option java_package = "api.review.v1";
enum ErrorReason {
// 设置缺省错误码
option (errors.default_code) = 500;
// 为某个枚举单独设置错误码
NEED_LOGIN = 0 [(errors.code) = 401];
DB_FAILED = 1 [(errors.code) = 500];
ORDER_REVIEWED = 100 [(errors.code) = 400];
}
之后生成代码
在diz层便可以使用生成的代码进行错误响应。
// CreateGreeter creates a Review, and returns the new Review.
func (uc *ReviewUsecase) CreateReview(ctx context.Context, review *model.ReviewInfo) (*model.ReviewInfo, error) {
uc.log.WithContext(ctx).Debugf("[biz] CreateReview, req:%v", review)
reviews, err := uc.repo.GetReviewByOrderID(ctx, review.OrderID)
if err != nil {
return nil, v1.ErrorDbFailed("查询数据库失败")
}
if len(reviews) > 0 {
// 已经评价过
fmt.Printf("订单已评价, len(reviews):%d\n", len(reviews))
return nil, v1.ErrorOrderReviewed("订单已:%d 已评价", review.OrderID)
}
review.ReviewID = snowflake.GenID()
return uc.repo.SaveReview(ctx, review)
}
这样便可以定义想返回的