Droplet——一款轻量的Golang应用层框架( 二 )

  • API handler中充斥了诸如 param, ok := req.Query("param") / param, ok := req.Header.Get("param") / err := xxx.Bind(req, &param) 之类的代码,这和业务毫无关系
    1. 没有请求/响应的结构化实体: 如果有开发过这些框架中间件的同学一定知道,大部分框架中间件的协议定义都是以 http.Request/httpResponse 为主体的,这意味着如果不做任何前置处理,你只能通过字节数组来感知 请求与响应 这在部分场景都不太方便,比如:根据请求、响应的结构体是否具备某些特征(比如接口)来执行某些特定的业务通用逻辑;又或者想在中间件中融入一些自动化的参数校验逻辑,因为你没有一个具体的结构化对象;再或者你不想要在每一个 API handler 中去设置一个响应的 Wrapper(通常它类似于 {code: 0, msg: "", data:{}}),想要在中间件去自动包装上它,也很难执行;最后就是——如果只依靠 http.Request/httpResponse,你也难在中间件感知到其他参与者的处理状态, 。
    相信我说的这些问题,使用过的同学应该都有所感触,而这些问题并非难以解决,它们中的大部分基本都是可以通过自行建立一套约定来得以缓解(比如将这些信息都通过 context 去获取),而 Droplet 也是诞生于我在过往团队中去克服这些问题的实践之中,是一个相对可靠的实现 。
    工作原理带着上面提到的这些问题,我们来看看 Droplet 的工作原理是怎样的,如下图所示:
    Droplet——一款轻量的Golang应用层框架

    文章插图
    如我所说的那样,Droplet 的核心在于 提供基于pipeline的请求/响应处理能力,因此我们可以看见这个图中涉及的所有模块都是基于 pipleline,可以说 Droplet 的所有能力都是由其扩展而来 。这里我们先介绍下图中出现的几个中间件(Middleware,这是组成 pipepine 关键元素):
    1. HttpInfoInjector: 注入 http 相关的一些信息,如 requestid, http.Request 等
    2. RespReshape: 根据 handler 的响应结果来进行一些调整,包括:发生错误时设置上默认的错误码、错误信息;如果缺少响应 wrapper 时包装上配置好的 wrapper
    3. HttpInput: 如果你设置了 API 的输入参数类型,那么该中间件会自动根据 Content-Type、 struct tag 来读取对应的参数值,同时自动使用 validator 来检测参数错误
    4. TrafficLog: 如名字所示,如果你配置了响应的 logger,那么该中间件会执行日志记录 。请注意该中间件工作在其他默认中间件的后面、你的handler之前,因此它统计的耗时是你业务函数的真正耗时,而不包含其他中间件的耗时时间,你可以考虑通过 网关 或 Mesh 来记录完整的接口耗时 。
    Tips
    • Middleware 处理请求和响应顺序是相反的——即第一个处理请求的中间件它会是最后一个处理响应的 。
    • 框架工作在应用层的优势有两点:
      • 与接入层框架解耦,保证绝项目代码可平滑 扩展/切换 其他接入层框架
      • 能够获取到结构化的接口 输入参数 与 输出参数 你可以对其进行更具精细的切面操作
    GetStart
    这里以 Gin 为例,其他框架类似 。
    首先获取对应 wrapper 的 submodule:
    go get github.com/shiningrush/droplet/wrapper/gin// if you want to ensure the droplet is latest, you can get dropletgo get github.com/shiningrush/droplet然后程序代码如下:
    package mainimport ( "reflect" "github.com/gin-gonic/gin" "github.com/shiningrush/droplet/core" "github.com/shiningrush/droplet/wrapper" ginwrap "github.com/shiningrush/droplet/wrapper/gin")func main() { r := gin.Default()// 使用 wrapper 包装原始的 API r.POST("/json_input/:id", ginwrap.Wraps(JsonInputDo, wrapper.InputType(reflect.TypeOf(&JsonInput{})))) r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")}type JsonInput struct {// 从 path 读取, 并且为必须参数 IDstring`auto_read:"id,path" json:"id" validate:"required"`// 从 header 读取, 并且为必须参数 Userstring`auto_read:"user,header" json:"user" validate:"required"`// 从 json unmarshal 后的ips字段读取 IPs[]string `json:"ips"`// 从 json unmarshal 后的 count 字段读取 Count int`json:"count"`// 读取原始的 http body,接收参数类型必须为 []byte or io.ReadCloser Body[]byte`auto_read:"@body"`}func JsonInputDo(ctx core.Context) (interface{}, error) { input := ctx.Input().(*JsonInput) return input, nil}

    经验总结扩展阅读