golang服务的整洁架构模板
模板的目的为了展示:
- 如何组织项目,以防止项目演化成难以维护的代码
- 在哪里处理业务逻辑,以维护代码的独立、清晰和可扩展
- 当微服务增长时,不要失去控制
使用了 Robert Martin 的原则
此模板实现了四种类型的服务器:
模板包含三个领域,演示多服务架构:
- 用户认证 — 注册、登录、基于 JWT 的授权
- 任务管理 — CRUD 操作,支持状态转换(todo、in_progress、done)
- 翻译 — 文本翻译与历史记录
所有领域在四种传输协议(REST、gRPC、AMQP RPC、NATS RPC)上均可用。
模板包含三个完整实现的领域,每个领域均可通过所有四种传输协议(REST、gRPC、AMQP RPC、NATS RPC)访问。
注册、登录和基于 JWT 的授权。
| 操作 | REST | gRPC |
|---|---|---|
| 注册 | POST /v1/auth/register |
AuthService/Register |
| 登录 | POST /v1/auth/login |
AuthService/Login |
| 获取资料 | GET /v1/user/profile |
AuthService/GetProfile |
- 密码使用 bcrypt 加密
- JWT 令牌支持可配置的过期时间
- 所有传输协议均有认证中间件
CRUD 操作,支持状态状态机。
| 操作 | REST | gRPC |
|---|---|---|
| 创建 | POST /v1/tasks |
TaskService/CreateTask |
| 列表 | GET /v1/tasks |
TaskService/ListTasks |
| 获取 | GET /v1/tasks/:id |
TaskService/GetTask |
| 更新 | PUT /v1/tasks/:id |
TaskService/UpdateTask |
| 状态转换 | PATCH /v1/tasks/:id/status |
TaskService/TransitionTask |
| 删除 | DELETE /v1/tasks/:id |
TaskService/DeleteTask |
- 状态转换:
todo→in_progress→done(以及in_progress→todo) - 支持
limit/offset分页和可选状态过滤 - 任务绑定到已认证的用户
通过外部 API 进行文本翻译,支持历史记录。
| 操作 | REST | gRPC |
|---|---|---|
| 翻译 | POST /v1/translation/do-translate |
TranslationHistoryService/DoTranslate |
| 历史 | GET /v1/translation/history |
TranslationHistoryService/ShowHistory |
# Postgres, RabbitMQ, NATS
make compose-up
# Run app with migrations
make run# DB, app + migrations, integration tests
make compose-up-integration-testmake compose-up-all Check services:
- AMQP RPC:
- URL:
amqp://guest:guest@127.0.0.1:5672/ - Client Exchange:
rpc_client - Server Exchange:
rpc_server
- URL:
- NATS RPC:
- URL:
nats://guest:guest@127.0.0.1:4222/ - Server Exchange:
rpc_server
- URL:
- REST API:
- gRPC:
- URL:
tcp://grpc.lvh.me:8081|tcp://127.0.0.1:8081 - v1/auth.proto
- v1/task.proto
- v1/translation.history.proto
- URL:
- PostgreSQL:
postgres://user:myAwEsOm3pa55@w0rd@127.0.0.1:5432/db
- RabbitMQ:
- http://rabbitmq.lvh.me | http://127.0.0.1:15672
- Credentials:
guest/guest
- NATS monitoring:
- http://nats.lvh.me | http://127.0.0.1:8222/
- Credentials:
guest/guest
配置和日志能力初始化。主要的功能在 internal/app/app.go
12-Factor推荐将应用的配置存储于 环境变量 中( env vars, env )。环境变量可以非常方便地在不同的部署间做修改,却不动一行代码;
与配置文件不同,不小心把它们签入代码库的概率微乎其微;与一些传统的解决配置问题的机制(比如 Java 的属性配置文件)相比,
环境变量与语言和系统无关。
設定:config.go
例如:.env.example
docker-compose.yml 使用 env 變數來配置服務。
Swagger 文档。由 swag 库自动生成 你不需要自己修改任何内容
Protobuf 文件。它们用于为 gRPC 服务生成 Go 代码。 这些 proto 文件也用于生成 gRPC 服务的文档。 您不需要自己修改任何内容。
集成测试 它会在应用容器旁启动独立的容器
这里只有通常只有一个 Run 函数在 app.go 文件种。它是 main 函数的延续
主要的对象在这里生成 依赖注入通过 "New ..." 构造 (阅读依赖注入),这个技术允许使用依赖注入的原则进行分层,使得业务逻辑独立于其他层。
接下来,我们启动服务器并阻塞等待_select_ 中的信号正常完成。
如果 app.go的规模增长了,你可以将它拆分为多个文件.
对于大量的依赖,可以使用wire
migrate.go 文件用于是数据库自动构建
它显示的包扣了 migrate 标签
go run -tags migrate ./cmd/app服务器处理层(MVC 控制器)。模板展示了 4 种服务器:
服务器路由器以相同的风格编写:
- 处理程序按应用领域分组(基于共同的基础)
- 为每个组创建自己的路由器结构,其方法处理路径
- 业务逻辑的结构被注入到路由器结构中,处理程序将调用它
简单的 RPC 版本控制。
对于 v2,我们需要添加 amqp_rpc/v2 文件夹,内容相同。
并在文件 internal/controller/amqp_rpc/router.go 中添加以下行:
routes := make(map[string]server.CallHandler)
{
v1.NewRoutes(routes, t, u, tk, j, l)
}
{
v2.NewTranslationRoutes(routes, t, l)
}简单的 gRPC 版本控制。
对于 v2,我们需要添加 grpc/v2 文件夹,内容相同。
还需要将 v2 文件夹添加到 docs/proto 中的 proto 文件中。
并在文件 internal/controller/grpc/router.go 中添加以下行:
{
v1.NewAuthRoutes(app, u, l)
v1.NewTaskRoutes(app, tk, l)
v1.NewTranslationRoutes(app, t, l)
}
{
v2.NewAuthRoutes(app, u, l)
v2.NewTaskRoutes(app, tk, l)
v2.NewTranslationRoutes(app, t, l)
}
reflection.Register(app)简单的 RPC 版本控制。
对于 v2,我们需要添加 nats_rpc/v2 文件夹,内容相同。
并在文件 internal/controller/nats_rpc/router.go 中添加以下行:
routes := make(map[string]server.CallHandler)
{
v1.NewRoutes(routes, t, u, tk, j, l)
}
{
v2.NewTranslationRoutes(routes, t, l)
}简单的 REST 版本控制
对于v2版本,我们需要添加restapi/v2文件夹,内容相同
在文件 internal/controller/restapi/router.go 中添加以下行:
apiV1Group := app.Group("/v1")
{
v1.NewRoutes(apiV1Group, t, u, tk, jwtManager, l)
}
apiV2Group := app.Group("/v2")
{
v2.NewRoutes(apiV2Group, t, u, tk, jwtManager, l)
}除了 Fiber,您可以使用任何其他 http 框架。
在 router.go 及以上的处理程序方法中,可以使用swag swagger 通过注释生成swagger文档.
业务逻辑实体(模型)可用于任何层. 这里包括一些方法,例如:参数检验.
业务逻辑.
- 方法按应用领域分组(在共同的基础上)
- 每个组都有自己的结构
- 一个文件对应一个结构 Repositories、webapi、rpc等业务逻辑结构被注入到业务逻辑结构中 (阅读 依赖注入).
是持久化存储的业务逻辑逻辑抽象,如数据库.
是webapi业务逻辑使用的抽象. 例如,它可能是业务逻辑通过 REST API 访问的另一个微服务。 包名称根据业务的实际用途进行命名
RabbitMQ RPC 模式:
- RabbitMQ 中没有路由
- 使用Exchange fanout出并绑定 1 个独占队列,这么配置是最高效的
- 在连接丢失时重新连接
为了去除业务逻辑对外部包的依赖,使用了依赖注入.
例如,通过New构造函数,我们将依赖注入到业务逻辑的结构中.
这使得业务逻辑独立(且可移植)
我们可以覆盖接口的实现,而无需更改 usecase 包.
它还将允许我们自动生成相关mock(例如使用 go.uber.org/mock),以便进行单元测试.
我们不依赖于特定的实现,以便始终能够将一个组件更改为另一个组件 如果新组件实现了接口,则业务逻辑无需更改。
package usecase
import (
// Nothing!
)
type Repository interface {
Get()
}
type UseCase struct {
repo Repository
}
func New(r Repository) *UseCase {
return &UseCase{
repo: r,
}
}
func (uc *UseCase) Do() {
uc.repo.Get()
}在编写完大部分代码后,程序员会意识到应用程序的最佳架构
一个好的架构允许尽可能晚地做出决策
依赖倒置的原理(与 SOLID 相同) 依赖的方向是从外层到内层。 因此,业务逻辑和实体可以保持独立于系统的其他部分。 因此,应用程序分为 2 层,内部和外部:
- 业务逻辑(Go标准库)
- 工具(数据库、服务器、消息代理、任何其他包和框架)
带有业务逻辑的内层应该是干净的,它应该有如下特征:
- 没有从外层导入的包
- 仅使用标准库的功能
- 通过接口调用外层(!)
业务逻辑对 数据存储 或特定的 Web API 是无感知的. 业务逻辑用抽象接口处理 数据库或 web API。 外层有其他限制:
- 该层的所有组件都不知道彼此的存在。如何进行组件间的调用呢? 只能通过业务逻辑的内层间接调用
- 所有对内层的调用都是通过接口进行的(!)
- 为了便捷地进行业务数据传输,数据格式得进行标标准化(
internal/entity)。
例如,您需要从 HTTP(控制器)访问数据库
HTTP 和数据库都在外层,这意味着他们彼此无法感知
它们之间的通信是通过usecase(业务逻辑)进行的:
HTTP > usecase
usecase > repository (repo)
usecase < repository (repo)
HTTP < usecase
更加复杂的业务逻辑
HTTP > usecase
usecase > repository
usecase < repository
usecase > webapi
usecase < webapi
usecase > RPC
usecase < RPC
usecase > repository
usecase < repository
HTTP < usecase
-
ENTITY是业务逻辑运行的结构。 它们位于
internal/entity文件夹中。 在 MVC 术语中,实体是models -
Use Cases 业务逻辑位于
internal/usecase中 与业务逻辑直接交互的层通常称为_infrastructure_ 层 这些可以是存储库internal/repo/persistent、外部 webapiinternal/repo/webapi、任何包和其他微服务。 在模板中,infrastructure 包位于internal/repo中
您可以根据需要决定你要调用的入口点,包括:
- controller
- delivery
- transport
- gateways
- entrypoints
- primary
- input
整洁架构 的经典版本是为构建大型单体应用程序而设计的,有 4 层
原版中,外层多分为两层,也使用依赖倒置原理 彼此(向内)通过接口进行通信
在复杂逻辑的情况下,内层也分为两层(通过接口进行分层)
复杂的工具可以通过分层设计。切记只有在你有需要的时候菜进行分层
除了整洁的架构, Onion architecture 和 Hexagonal (接口适配层) 一样能达到目的,他们都符合依赖倒置的原则 这三种模式都非常接近,不同的知识术语不同


