在了解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 ./...
)启动。
在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