引言
自从转到机器学习平台方向以后,技术栈也从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内。