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

Github地址
如标题所描述的 , Droplet 是一个 轻量 的 中间层框架,何为中间层呢?通常来说,我们的程序(注意这里我们仅仅讨论程序的范围,而非作为一个系统,因此这里不设计如 LB、Gateway、Mesh等内容 , 因为它们都处于程序以外)按不同的职责可以分为不同的层次,而按照不同的设计风格,常见的如下:
  • 三层架构:UIL(UserInterfaceLayer), BLL(BusinessLogicLayer), DAL(DataAccessLayer)
  • DDD分层架构(参考ddd-oriented-microservice):ApplicationLayer , DomainLayer,InfrastructureLayer
  • 洋葱架构(参考Onion Architecture ):Application, Infrastructure, ApplicationService, DomainService, DomainModel 。
Tips
洋葱架构其实也是基于DDD的,它是DDD分层架构的升级版本 。
但是今天我想用于解释中间层的架构并非以上的任何一种 , 它也源自于DDD的分层架构 , 不过我配合了六边形架构来说明它 , 分层图如下:
Droplet——一款轻量的Golang应用层框架

文章插图
在六边形架构中有个规则:依赖只能是由外部指向内部 。因此从外层到最内层分别是:
分层职责Access程序的接入层(在六边形架构中这被称为输入适配器) , 通常位于整个请求 or 任务的起点,它可能是某种Web框架,也可能是一些队列的消费框架等 。Application程序应用层 , 包含了一些非业务的逻辑 , 如:业务逻辑的编排、参数绑定、校验、请求日志、链路上报、状态读取等等Domain & Utils在最中心的地方我放入了两个层次描述:Domain 与 Utils , 这两个分层都应该是位于依赖的最底层,意味着他们不应该引用本项目的其他层次 。Domain层主要包含核心的业务逻辑,而Utils则是一些程序任何地方都可能会引用的代码段 , 比如常量定义、数据结构和语法糖等等Infrastructure基础设施层(在六边形架构中这被称为输出适配器) , 程序所有需要对外进行信息交换 or 功能依赖时都会放置在这一层实现,通常来说这些功能都是被依赖的那部分,因此我们如果要满足依赖约束的话 , 这里必须要引入 DIP(Dependency inversion principle) , 即在Application、Domain中定义依赖,而 Infrastructure 来实现它们,这样保证了它们是可被替换的六边形架构优点在于解耦程序中业务无关的部分,以保证它们都是可被替换与扩展的 。而 Droplet 就工作在 Application 层,它的核心能力只有一个:提供基于pipeline的请求/响应处理能力 。可能有人会疑问,几乎每个框架都会实现类似的能力,为什么我们需要 Droplet 呢?别急,我们来看看这些框架自带的 pipeline/middleware 存在什么弊端 。根据上面的架构图我们可以知道诸如 gin、go-restful、fasthttp 之类的http框架都是工作在 Access 层,因此框架自带的 pipeline/middleware 存在以下两个弊端:
  1. 框架绑定: 这个很容易理解,这些机制只能工作于特定的框架下,如果切换框架则需要需要调整代码 , 除了中间件的代码外 , 我时常也会见到程序在 API Handler 中耦合了大量框架相关的代码,比如:读取参数(header, query, body等)、根据业务结果回写响应等 , 这些代码渗透到了业务程序中(有时它们甚至会比业务代码占用了更多的行数) , 这加大了业务开发同学的维护成本 , 同时也降低了程序的可扩展性 。
一些相关的BadCase
想象一下:
  • 你一直在使用 gin,但是有一天运营拿着数据找到你,说机器占用的成本太高了,而你发现只要切换到 fasthttp 就能为你带来更高的性能,但是从 gin -> fasthttp 你需要调整大量的 API handler 代码,这可太让人头疼了 。
  • 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(通常它类似于

    推荐阅读