Golang API框架

在了解Golang的过程中,我决定制作该应用程序的框架,这对我以后的使用非常方便。 在我看来,结果是一个好工件,我决定与他人分享,同时讨论框架创建过程中出现的时刻。


图片


原则上,Go语言的设计表明它不需要开发大型应用程序(我的意思是缺少泛型,并且错误处理机制不是很强大)。 但是我们仍然知道,应用程序的大小通常不会减少,但通常情况恰恰相反。 因此,最好立即创建一个框架,在该框架上可以在不牺牲代码支持的情况下对新功能进行字符串化。


我尝试在文章中插入较少的代码,而是在Github上添加了指向特定代码行的链接,以希望可以更方便地查看整个图片。


首先,我为应用程序制定了一个计划。 由于在文章中我将分别讨论每个项目,因此我将首先列出该列表中的主要内容。


  • 选择包管理器
  • 选择用于创建API的框架
  • 选择依赖注入工具(DI)
  • Web请求路由
  • 根据请求标头的JSON / XML响应
  • ORM
  • 移居
  • 为模型层创建基类Service-> Repository-> Entity
  • 基本的CRUD存储库
  • 基本CRUD服务
  • 基本CRUD控制器
  • 要求验证
  • 配置和环境变量
  • 控制台命令
  • 记录中
  • 记录仪与Sentry或其他警报系统集成
  • 设置错误警报
  • 通过DI重新定义服务的单元测试
  • 百分比和测试覆盖率代码图
  • 招摇
  • Docker撰写

包装经理


阅读了各种实现说明 ,我选择了govendor ,此刻我对选择感到满意。 原因很简单-它允许您将依赖项与应用程序一起安装在目录内,存储有关软件包及其版本的信息。


有关软件包及其版本的信息存储在一个vendor.json 文件中 。 这种方法也有一个缺点。 如果您添加一个带有其依赖项的程序包,则连同有关该程序包的信息,有关其依赖项的信息也将进入文件中。 该文件增长迅速,不再可能清楚地确定哪些依赖项是主要的,哪些是派生的。


在PHP composer或npm中,主要依赖项在一个文件中描述,所有主要和派生依赖项及其版本都自动记录在锁定文件中。 我认为这种方法更方便。 但是到目前为止,govendor的实现对我来说已经足够了。


构架


从框架中,我不需要很多东西,一个方便的路由器,可以验证请求。 所有这一切都在流行的杜松子酒中发现。 他停了下来。


依赖注入


有了DI,我不得不承受一点痛苦。 首先选择Dig。 起初,一切都很棒。 所描述的服务,Dig进一步方便地构建了依赖关系。 但是后来发现,例如在测试期间无法重新定义服务。 因此, 最后得出的结论是,我采用了一个简单的服务容器sarulabs / di


我只需要分叉它,因为开箱即用它允许您添加服务并禁止重新定义它们。 在我看来,编写自动测试时,像在应用程序中那样初始化容器,然后重新定义一些服务,而改为指定存根,会更加方便。 在fork中,他添加了一种方法来覆盖服务描述。


但是最后,无论是对于Dig还是在服务容器中,我都必须将测试放入单独的程序包中。 否则,结果证明测试是在程序包中单独运行( go test model/service ),但是由于在这种情况下会产生循环依赖性,因此它们不会立即针对整个应用程序( go test ./... )启动。


根据请求标头的JSON / XML响应


在Gin中,我没有找到这个,所以我只是向基本控制器添加了一个方法 ,该方法根据请求标头生成响应。


 func (c BaseController) response(context *gin.Context, obj interface{}, code int) { switch context.GetHeader("Accept") { case "application/xml": context.XML(code, obj) default: context.JSON(code, obj) } } 

ORM


用ORM并没有选择的长久折磨。 有很多选择。 但是根据功能的描述,我喜欢GORM,它是选择时最受欢迎的功能之一。 支持最常用的DBMS。 至少肯定有PostgreSQL和MySQL。 它还具有用于管理创建迁移时可以使用的基本架构的方法。


移居


对于迁移,我选择了gorm-goose软件包。 我在全球范围内放置了一个单独的程序包,并开始向其迁移。 最初,这样的实现很尴尬,因为必须在单独的db / dbconf.yml文件中描述与数据库的连接。 但是后来发现,其中的连接字符串可以用从环境变量中获取值的方式来描述。


 development: driver: postgres open: $DB_URL 

这很方便。 至少对于docker-compose,我不必重复连接字符串


Gorm-goose还支持迁移回滚,我发现这非常有用。


基本的CRUD存储库


我更喜欢将涉及资源的所有内容放置在单独的存储库层中。 我认为,采用这种方法,业务逻辑代码会更干净。 在这种情况下,业务逻辑代码仅知道它需要处理从存储库获取的数据。 对于存储库中发生的事情,业务逻辑并不重要。 该存储库可以与关系数据库,KV存储,磁盘或其他服务的API一起使用。 在所有这些情况下,业务逻辑代码都是相同的。


CRUD存储库实现以下接口


 type CrudRepositoryInterface interface { BaseRepositoryInterface GetModel() (entity.InterfaceEntity) Find(id uint) (entity.InterfaceEntity, error) List(parameters ListParametersInterface) (entity.InterfaceEntity, error) Create(item entity.InterfaceEntity) entity.InterfaceEntity Update(item entity.InterfaceEntity) entity.InterfaceEntity Delete(id uint) error } 

也就是说,CRUD实现了Create()Find()List()Update()Delete() GetModel()GetModel()方法。


关于GetModel() 。 有一个基本的CrudRepository存储库,可实现基本的CRUD操作。 在将其嵌入到自己的存储库中,足以表明他们应该使用哪种模型。 为此, GetModel()方法必须返回GORM模型。 然后,我们必须在CRUD方法中使用反射使用GetModel()的结果。


举个例子


 func (c CrudRepository) Find(id uint) (entity.InterfaceEntity, error) { item := reflect.New(reflect.TypeOf(c.GetModel()).Elem()).Interface() err := c.db.First(item, id).Error return item, err } 

也就是说,实际上,在这种情况下,有必要放弃静态类型,而采用动态类型。 在这样的时刻,人们特别感到语言中缺少泛型。


为了使使用特定模型的存储库能够实现自己的用于过滤List()方法中的List()的规则,我首先实现了后期绑定,以便从List()方法中调用负责构造查询的方法。 并且可以在特定的存储库中实现此方法。 很难以某种方式放弃使用其他语言时形成的思维模式。 但是,以崭新的眼光看待它,并欣赏所选路径的“优雅”,然后他将其重新采用了一种更接近Go的方法。 为此,只需通过接口在CrudRepository 声明一个查询构建器 ,该构建器 List()


 listQueryBuilder ListQueryBuilderInterface 

事实证明,这很有趣。 将语言限制为后期绑定(起初似乎是一个缺陷),这会鼓励更清晰地分离代码。


基本CRUD服务


这里没有什么有趣的,因为框架中没有业务逻辑。 可以将CRUD方法对存储库的调用简单地代理


在服务层中,必须实现业务逻辑。


基本CRUD控制器


控制器实现CRUD方法 。 他们处理来自请求的参数,将控制权转移到相应的服务方法,然后根据服务的响应,形成对客户端的响应。


对于控制器,我的故事与关于过滤列表的存储库的故事相同。 结果,我用自制的后期绑定重新实现,并添加了一个hydrator ,它根据请求参数形成带有用于过滤列表的参数的结构。


在CRUD控制器随附的水化器中,仅处理分页参数。 在集成了CRUD控制器的特定控制器中,可以重新定义水化器。


要求验证


验证由Gin执行。 例如,添加记录( Create()方法)时,足以装饰实体结构元素


 Name string `binding:"required"` 

框架的ShouldBindJSON()方法负责检查请求参数是否符合装饰器中所述的要求。


配置和环境变量


我真的很喜欢Viper的实现,尤其是与Cobra结合使用时。


阅读我在main.go中描述的配置 base.env文件中描述不包含机密的基本参数。 您可以在添加到.gitignore的.env文件中覆盖它们。 在.env中,您可以描述环境的秘密值。


环境变量具有更高的优先级。


控制台命令


对于控制台命令的描述,我选择了Cobra 。 比将Cobra与Viper一起使用更有益。 我们可以描述命令


 serverCmd.PersistentFlags().StringVar(&serverPort, "port", defaultServerPort, "Server port") 

并将环境变量绑定到命令参数的值


 viper.BindPFlag("SERVER_PORT", serverCmd.PersistentFlags().Lookup("port")) 

实际上,此框架的整个应用程序都是控制台。 通过服务器控制台命令之一启动Web服务器。


 gin -i run server 

记录中


我选择logrus包进行日志记录 ,因为它具有我通常所需的一切:设置日志记录级别,记录位置,添加钩子(例如,将日志发送到警报系统)。


记录仪与警报系统集成


我之所以选择Sentry,是因为由于与logrus: logrus_sentry集成在一起,因此一切变得非常简单。 我使用SENTRY_DSN Sentry SENTRY_DSN的URL和发送到Sentry SENTRY_TIMEOUT的超时进行了参数 SENTRY_TIMEOUT 。 事实证明,默认情况下,超时很小(如果没有记错的话)为300毫秒,并且许多邮件未传递。


设置错误警报


我分别对Web服务器控制台命令进行了紧急处理。


通过DI重新定义服务的单元测试


如上所述,必须为单元测试分配一个单独的程序包。 由于选择的用于创建服务容器的库不允许重新定义服务,因此在fork中添加了一种重新定义服务描述的方法。 因此,在单元测试中,您可以使用与应用程序中相同的服务描述。


 dic.InitBuilder() 

并以此方式在存根中仅重新定义一些服务描述


 dic.Builder.Set(di.Def{ Name: dic.UserRepository, Build: func(ctn di.Container) (interface{}, error) { return NewUserRepositoryMock(), nil }, }) 

接下来,您可以构建一个容器并在测试中使用必要的服务:


 dic.Container = dic.Builder.Build() userService := dic.Container.Get(dic.UserService).(service.UserServiceInterface) 

因此,我们将测试userService,它将使用提供的存根代替实际的存储库。


百分比和测试覆盖率代码图
我对标准的go测试实用程序完全满意。


您可以单独运行测试


 go test test/unit/user_service_test.go -v 

您可以一次运行所有测试


 go test ./... -v 

您可以构建覆盖图并计算覆盖率


 go test ./... -v -coverpkg=./... -coverprofile=coverage.out 

并在浏览器中查看代码覆盖图以及测试


 go tool cover -html=coverage.out 

招摇


有一个针对Gin的gin-swagger项目,可用于生成Swagger规范并基于该文档生成文档。 但是,事实证明,为了生成特定操作的规范,有必要指出关于控制器特定功能的注释。 事实证明这对我来说不是很方便,因为我不想在每个控制器中复制CRUD操作代码。 相反,在特定的控制器中,我只是如上所述嵌入了CRUD控制器。 我也不想为此创建存根函数。


因此,我得出的结论是,规范是使用goswagger生成的,因为在这种情况下, 可以在不依赖于特定功能的情况下描述操作。


 swagger generate spec -o doc/swagger.yml 

顺便说一下,使用goswagger,您甚至可以从相反的角度出发,并基于Swagger规范生成Web服务器代码。 但是通过这种方法,使用ORM遇到了困难,我最终放弃了它。


使用gin-swagger生成文档,为此指示了预先生成的规范文件。


Docker撰写


在框架中,我为代码和base添加了两个容器的描述。 在带有代码的容器的开头,我们等待直到带有基础的容器完全启动。 并且在每次开始时,如有必要,我们都会进行迁移。 如上所述,在dbconf.yml中描述了连接数据库以进行迁移的参数,可以在其中使用环境变量来传输用于连接数据库的设置。


谢谢您的关注。 在此过程中,我不得不适应语言的功能。 我想知道在Go上花费更多时间的同事的意见。 当然,可以使某些时刻变得更加优雅,所以我将对有用的批评感到高兴。 链接到框架: https : //github.com/zubroide/go-api-boilerplate

Source: https://habr.com/ru/post/zh-CN455302/


All Articles