Nunu-Go 应用脚手架实践

引言

自从转到机器学习平台方向以后,技术栈也从Java转到了Golang + K8S + MLOps。但无论怎么转,只要做的还是面向普通用户的平台/应用,Web应用研发依然是不可缺少的一环。Web应用研发这个词可大可小,往小了说是写几个HTTP服务,往大了说是写一个可以处理好大量服务间调用关系、具有高可靠性、业务可扩展的工程,如果是后者,还是需要一定的框架加持。Golang里面没有像Java里面SpringBoot的大一统框架,对于刚接触Golang的朋友而言,需要去github的各个角落搜寻框架,最后组装到一起进行开发。但如果你想一步到位,直接找个已经把主流框架集成好的脚手架工程,不妨了解一下今天这位主角——Nunu

简介

Nunu是一个基于Golang语言的应用脚手架工程,它里面集成了开源社区里一些主流的框架与库,另外基于MVC的开发模式对工程进行了层次划分,便于使用者更科学合理地编写业务逻辑,最后工程的易用性方面也做了优化,如增加了代码热更新、命令行工具、一键构建子模块等。

 

支持的框架与库如下:

  • Gin:HTTP服务,主要包括请求路由、参数解析/验证、拦截器、服务映射等。
  • Gorm:持久化与对象映射,将数据库表结构/操作与代码进行映射,开发者操作Go代码即可实现数据库的修改。
  • Wire:依赖注入,负责全局对象的生成与组装,开发者不用再关注这些对象之间的依赖关系。
  • Viper:配置管理,可以对各种格式(如JSON、YAML、XML等)的配置文件进行读取与解析的库。
  • Zap:高性能日志,可定义日志级别、输出格式、输出管道等。
  • Lumberjack:日志归档,将日志按照时间、容量大小、保留时长、备份数等规则进行切分和保存。
  • Gocron:定时任务调度,按照时间周期或crontab语法控制任务定时调度。
  • Testify:测试,提供了一系列辅助代码测试的库。
  • Gomock:mock,对接口进行mock,一般与测试框架合用进行单元测试。
  • Swaggo:API文档自动生成工具,基于Gin服务方法上的注释,生成HTTP接口的地址、请求参数和响应参数文档。
  • 详见

工程结构

Nunu提供了一个命令行工具,可以选择从一个空白的工程开始,按照你所需要的业务领域(比如User、Permission、Shop、Job),一步步生成你的工程,也可以选择直接生成一个大而全的示例工程。无论哪种方式,生成的工程结构是一样的,如下所示:

.
├── api                 // API接口定义,工程透出的SDK
│ └── v1
├── cmd                 // 应用的入口点,根据不同的命令进行不同的操作
│ ├── migration         // 执行数据库迁移
│ ├── server            // 启动服务器
│ │ ├── wire              // 依赖注入
│ │ │ ├── wire.go           // 依赖关系定义
│ │ │ └── wire_gen.go       // 依赖生成
│ │ └── main.go           // 主函数
│ └── task              // 定时任务
├── config              // 应用的配置文件,根据不同的环境(如开发环境和生产环境)提供不同的配置。
├── deploy              // 用于部署应用,包含了一些部署脚本和配置文件。
├── docs                // 存放工程相关文档
├── internal            // 应用的核心模块,包含了各种业务逻辑的实现。
│ ├── handler           // 处理HTTP请求的处理器,负责接收请求并调用相应的服务进行处理。
│ ├── middleware        // 前置与后置拦截器的实现
│ ├── model             // 数据模型的定义。
│ ├── task              // 定时任务逻辑的视线
│ ├── repository        // 数据访问层的实现,负责与数据库进行交互。
│ ├── server HTTP       // 服务器的实现。
│ └── service           // 业务逻辑的实现,负责处理具体的业务操作。
├── pkg                 // 通用的功能和工具。
├── scripts             // 脚本文件,用于项目的构建、测试和部署等操作。
├── test                // 各个模块的单元测试,按照模块划分子目录。
│ ├── mocks
│ └── server
├── web                 // 前端相关的文件,如HTML、CSS和JavaScript等。
├── Makefile
├── go.mod
└── go.sum

分层架构

这里想着重说明一下Nunu里的分层架构,因为它关系到我们业务代码具体如何写比较合适。正如Nunu Layout里展示的,Handler, Service, Repository其实是典型的MVC分层模式:

  • Handler:负责接收HTTP请求参数,对参数进行校验后,调用Service层进行处理,得到处理结果后,将其封装成API所定义的格式返回给前端。
  • Service:负责核心业务逻辑处理,一般内部会注入几个业务领域的Repository,如一个订单服务OrderService的Create操作,先会调用UserRepository.Get获取发起人的信息,再调用GoodRepository.Get获取商品信息,然后调用OrderRepository.Create创建订单,最后调用EventRepository.Send向消息队列发送创建成功消息。
  • Repository:负责与更后台进行交互,如数据库、日志存储引擎、消息队列、用户系统、第三方服务等,这一层需要尽量保证没有业务逻辑的操作。

改进建议

使用Nunu有一段时间了,在享受它带来的便利的同时,我也根据自己的需要进行过一些改造,下面是几个改造经验:

1 引入Validator层

Handler和Service层之间可以再加一层Validator,它本质是从Handler里细分出来的一层,主要负责对参数进行校验,这里的校验既包括参数格式方面的基本校验,也包含一些业务上合理性的校验。Validator可以被多个Handler注入,实现代码复用,如权限、用户这些通用Validator是再适合不过了。

2 引入pkg/client包

引入pkg/client包:pkg中可以再加一个包client,用于存放与后台服务对接的SDK,一方面有些服务可能需要我们自己写http发送/解析请求,这部分逻辑太细,不太适合直接放到Repository中,另一方面SDK中往往含有后台服务特定的语义,比如后台服务如果是 DockerRegistry(镜像仓库),在删除某个镜像时需要知道该镜像的digest值才行,而这个信息过于专业,并不适合上层的Repository感知到,Repository只用传递image名称(如nginx:v1),再调用Delete方法即可,至于digest怎么获取,由底层DockerRegistry的SDK这一层去消化掉。

3 参数载体细分

Nunu原生只有Model,但Model理论上只应作为Repository的操作单元,而不应该给靠近前端的Service及Handler使用,可以考虑再抽取下面三种参数载体:

  • Request:用于Handler/Service层,传递请求参数,如 ListOrdersRequest{ UserId, Status, StartTime, EndTime }
  • Response:用于Handler层,作为API的响应参数,如 ListOrdersResponse{ Orders, TotalCount }
  • Dto:用于Handler/Service层,Dto可以继承自Model,Dto根据业务需要添加其他字段,比如最典型的一个场景,OrderService.Get需要返回一个订单的信息,从OrderRepository.Get取出的订单Model只有一个UserId字段(为了不冗余存储,通常数据库表只用外键ID关联),而我们还需要用户的昵称,那这时就需要在OrderService中再调用UserRepository得到该字段,并设置到订单Dto内。