Skip to content

架构

介绍

理解您的开发工具并了解后台发生的情况至关重要。掌握您的工具和环境可以极大地降低错误风险,简化调试,并帮助您的代码与框架和谐工作。本节的目的是为您提供框架整体功能和设计的概述,让您在使用时更加舒适和自信。

术语

本节将简要解释文档中使用的一些技术词汇。如果在阅读文档时不确定某个术语的含义,请随时参考本节。

生命周期:从开始到结束的执行过程,包含中间步骤。

组件:表示层(HTTP/REST)的一部分结构,实现了 goyave.Composable 接口,允许它们访问基本的服务器资源。一个组件可以是多个子组件的父级。

处理器:接收传入请求和响应写入器的函数。可以为同一个请求执行多个处理器。

控制器:实现一个或多个处理器的结构。控制器的职责仅限于表示层。

中间件:在控制器处理器之前执行的处理器。中间件可以拦截请求,修改其数据,并在到达控制器处理器之前发送响应。

路由器:根级处理器,负责解析请求 URI 并匹配相应的路由。

路由:链接到控制器处理器的 URI 定义。如果此路由匹配传入请求,路由器将执行相关的控制器处理器。

应用程序:使用 Goyave 框架作为库的程序。

模型:反映数据库表结构的结构。模型的实例是单个数据库记录。

仓库:实现用于抽象资源数据库操作的方法的结构。

数据填充器:在数据库中创建若干随机记录的函数。

迁移:在数据库(或数据层)的上下文中,指增量模式修改(创建表、添加/删除列等)。

:具有特定职责的代码的隔离部分。层之间相互通信,但它们不直接相互依赖。

DTO:数据传输对象。在多个层之间携带数据的结构。

服务:实现业务逻辑(例如 UserService 管理 User 资源)或对功能(例如:AuthService 管理认证)或外部依赖(例如:其他微服务、第三方 API)的抽象的结构。

生命周期

初始化

应用程序生命周期的第一步是加载资源和配置。它要么由应用程序开发人员手动完成,要么默认情况下在创建服务器时由框架自动处理 goyave.New()

  • goyave.New()
    • 然后加载应用程序的配置文件,覆盖默认值。
    • 加载语言文件。默认情况下,框架内提供 en-US 语言,并用作默认语言。框架将在工作目录中查找自定义语言文件,加载可用的语言,并在需要时覆盖 en-US 语言条目。
    • HTTP 服务器已初始化,但尚未在网络上监听。网络监听器仅在稍后调用 server.Start() 时创建。路由器也已创建,但尚未包含任何路由。在底层,Goyave 使用 net/http*http.Server。因此,路由器实现了 http.Handler
    • 如果配置未将 none 指定为数据库连接类型,则创建数据库连接池。
  • 可选地,为优雅关闭注册 OS 信号钩子,用于 SIGINTSIGTERM。还可以注册其他钩子,如启动和关闭钩子。
  • 服务被初始化和注册。
  • 路由被注册。
  • goyave.Start() 启动服务器。
    • 创建网络监听器。
    • HTTP 开始服务请求。
    • 启动一个 goroutine,并在其中按注册顺序执行所有启动钩子。
    • 注意server.Start()server.Stop()server.IsReady() 是并发安全的操作。

请求

本节将解释传入 HTTP 请求的生命周期。

INFO

每个请求都在其自己的 goroutine 中处理。

路由

当收到请求时,调用路由器的 ServeHTTP() 方法。路由器尝试使用请求的 URI 和方法将其与注册的路由匹配。同时,它解析潜在的路由参数并将其存储以供处理器将来使用。路由器匹配算法在路由文档中有更详细的解释。

有两个特殊路由:"Not found" 和 "Method not allowed"。它们在路由文档中有更详细的解释。因此,路由器在收到请求时总是执行一个路由。

包装

一旦路由器决定了要执行的路由,就会创建两个包装器对象。这些元素是框架的基本特性:

  • *goyave.Request:用于检索请求信息和正文读取器。它还可以存储额外的请求范围信息,例如认证用户、验证错误等,这些信息随后在请求的生命周期内传播到所有处理器。
  • *goyave.Response:用于写入响应。

处理器栈

接下来,生成一个处理器。在栈的顶部,我们有全局中间件(为每个请求执行的中间件,即使匹配的路由是 "Not found" 或 "Method not allowed")。然后是继承自父路由器的中间件。在它们下面,我们有专门应用于匹配路由的中间件。最后在栈的底部,我们有控制器处理器。执行从栈顶开始,向下然后向上返回。这意味着中间件如果愿意,也可以在控制器处理器返回后执行代码。

中间件栈图

框架包含两个内置的全局中间件,它们总是在任何路由器中注册:恢复语言中间件。

恢复: 此中间件确保处理任何未恢复的 panic。如果在发生 panic 时从不返回响应,服务器将包装错误,记录它,并将响应状态设置为 500 Internal Server Error,从而触发相关的状态处理器的执行,该处理器将优雅地处理错误。

语言: 检查 Accept-Language 标头。如果存在,则解析其值,并相应地设置请求的语言字段,以便在后续处理器中轻松进行本地化。如果标头缺失、无效或请求不支持的语言,框架将回退到配置中定义的默认语言。了解更多请点击这里

上下文传播

Goyave 充分利用了上下文 API。服务器实例有一个基础上下文,该上下文传播到每个请求。因此,每个请求都有其自己的子上下文,范围限定于该请求。然后,此上下文在应用程序中各处以及各层之间传播,这些层在下面解释。

将上下文传播到各处允许跨层传输数据,而无需创建依赖关系或需要不属于它们的额外方法参数。因为这些数据存储在与当前请求关联的上下文中,所以它范围限定于该请求。因此,数据库或外部服务请求也可以使用请求的上下文。例如,这对于事务系统很有用。

在瞬态共享空间中存储数据的能力并不意味着一切都应该存储在那里。此特性应谨慎使用。

INFO

在以下非详尽示例情况下,将数据存储在上下文中有意义:

  • 向结构化记录器添加范围限定的属性
  • 存储业务事务状态
  • 存储遥测数据,例如跟踪跨度

它还确保上下文取消得到整个由请求启动的过程的正确支持和处理。如果连接意外中断、客户端取消请求、请求超时或服务器关闭,请求上下文可以被取消。

最终化

当栈顶返回时,请求进入最终化阶段。

  • 如果响应为空且未设置状态,则写入 204 No Content
  • 如果已为响应定义了状态码但响应正文为空,则将执行与此代码关联的状态处理器(如果存在)。简而言之,状态处理器是处理一个或多个响应状态码的集中方式。
  • 最后,关闭响应写入器。了解为什么这很重要,请参阅链式写入器文档

响应

Goyave 提供了一个链式写入器系统,允许多个写入器读取或更改原始响应正文。例如,您可以在链中添加一个 gzip 写入器,这样您的响应将被压缩,而无需在处理器中指定它。另一个用例是用于记录:写入器将读取响应正文以了解其长度,而不更改它,并在最终化阶段关闭写入器时打印结果。

注意

重要的是要记住,所有写入总是首先通过 *goyave.Response

通常,响应的写入器在中间件中被替换。从 *goyave.Response 中获取当前写入器,并用作新创建的链式写入器的目标。然后将此新链式写入器设置在 *goyave.Response 中。

链式写入器图

写入器在最终化阶段结束时关闭,告诉它们应用程序完全完成了此请求。

预写

有一个 PreWrite() 钩子,允许在响应 HTTP 标头写入之前更改响应标头或状态。一旦写入,它们就被锁定直到请求生命结束:只有正文可以写入。这允许框架仅在向正文写入内容或请求最终化时发送状态标头。

例如,gzip 链式写入器将确保在实际写入正文之前设置从未压缩数据推导出的 Content-Type 标头。

关闭

当调用 server.Stop()、触发 OS 信号处理器(如果设置)或底层 http.Server 返回错误时,服务器停止。活动连接不会中断,但不会接受新连接。

server.Start() 在执行所有关闭钩子(按注册顺序)后返回。关闭钩子在与调用 server.Start() 的 goroutine 相同的 goroutine 中执行。如果有任何数据库连接,它也会干净地关闭。

概述

本节将解释 Goyave 应用程序的一般架构。应用程序分为三个不同的层

  • 表示层:HTTP/REST 层,是您应用程序的外观
  • 领域/业务层:包含服务
  • 数据层:使用仓库与数据库交互,并包含模型

每个层不直接依赖其他层,因为它们定义了代表自身需求的接口。以下图表描述了请求在 Goyave 应用程序中的通常流程。

架构概述图

这种架构有几个优点:

  • 良好的关注点分离,无直接依赖
  • 易于测试
  • 数据层不会泄漏到业务层,即使涉及事务
  • 降低暴露不应公开信息的风险
  • 易于阅读、探索和维护
  • 因为没有任何东西是全局的,消除了对 goroutine 同步的高成本需求

DTO

表示层和领域层使用称为 DTO(数据传输对象)的结构进行通信。一旦输入数据经过验证和清理,表示层将将其转换为 DTO 并传递给领域层。

DTO 在其自己的独立包中定义,因为它们确实是用于在不同层之间通信的对象。框架提供了几种工具来使这些转换变得轻松。了解更多信息,请参阅 DTO 和模型映射文档

表示层 (HTTP/REST)

表示层是您应用程序的外观,是通往外部世界的门。其目的是确保输入数据完整性和正确的响应格式和内容。它包含所有与 HTTP 协议相关的代码。

组件

在 Goyave 中,没有任何东西是全局的。这意味着需要一种机制,以便服务器的基本资源(如配置、记录器等)可以分发给服务器的每个组件。这种机制实际上称为组件,并由接口 goyave.Composable 描述。

表示层中的大多数结构实际上都是 Goyave 组件。通过组合 goyave.Component 结构,可以轻松地将结构转变为组件。一个组件可以是多个子组件的父级。组件由框架通过 Init() 方法初始化。

领域/业务层

在这一层中,实现了服务。这是您应用程序核心逻辑和价值所在。服务是实现业务逻辑(例如 UserService 管理 User 资源)或对功能(例如:AuthService 管理认证)或外部依赖(例如:其他微服务、第三方 API)的抽象的结构。

这一层在其他两层之间架起桥梁。它接受 DTO 作为输入并返回 DTO 作为输出。当需要与数据层通信时,它使用模型。

WARNING

重要的是领域层永远不会泄漏模型!

会话

服务可以利用框架提供的会话机制。该系统创建事务系统(无论是数据库还是其他)的抽象,以便服务可以定义和控制业务事务,而无需直接与数据库交互。

INFO

事务是作为一个工作单元执行的一个或多个操作的序列。仅当所有步骤都成功时,最终结果才被验证并写入数据库。在这种情况下,我们说事务被提交。如果其中一个步骤失败,则事务被回滚

因此,下一节中解释的仓库不需要担心是否在事务内部工作。这样,服务可以调用多个仓库操作,以任何顺序,甚至可能来自多个不同的仓库或其他服务,同时保持对其自身业务事务的控制。

数据层

数据层包含模型仓库和所有与数据相关的代码。模型是数据库模式的 Go 表示。仓库实现将被调用来处理数据(获取、创建、更新、删除等)的方法。

INFO

Goyave 使用 Gorm ORM 构建。这并不阻止您在需要时使用原始 SQL。

数据库连接由框架管理并且是长连接的(池)。当服务器关闭时,数据库连接会自动关闭。因此,您不必担心在应用程序中创建、关闭或刷新数据库连接。

依赖注入

Goyave 不使用任何复杂的东西进行依赖注入。仅使用原生 Go,无代码生成。

对于表示层

对于组件,当框架从其任何函数接收组件时,服务器依赖项通过 Init() 方法自动注入。

Goyave 为服务提供了一个简单的依赖容器,组件可以随时从服务器访问(最好从它们的 Init() 方法访问)。在生命周期的初始化阶段,服务被创建并注册到此容器中。

对于业务层

仓库和服务在初始化时创建。它们不使用依赖容器,因为它们不依赖于 Goyave 服务器。相反,它们以 New() 构造函数参数的形式接受它们的依赖项,以它们定义的接口形式。

INFO

您可以在服务文档中了解更多关于此系统的信息。

目录结构

以下是 Goyave 应用程序的推荐目录结构

. ├── database (数据层) │ ├── migrations │ │ └── ... │ ├── model │ │ ├── user.go │ │ └── ... │ ├── repository │ │ └── ... │ └── seed (可选) │ └── seed.go ├── dto │ ├── user.go │ └── ... ├── http (表示层) │ ├── controller │ │ └── user │ │ ├── user.go │ │ └── validation.go │ ├── middleware │ │ └── ... │ ├── route │ │ └── route.go │ └── validation │ └── ... ├── resources │ └── lang │ └── en-US (语言名称) │ ├── fields.json (可选) │ ├── locale.json (可选) │ └── rules.json (可选) ├── service (领域层) │ ├── user │ │ └── user.go │ └── service.go ├── .gitignore ├── config.json ├── go.mod ├── go.sum ├── main.go └── README.md

  • dto 包含 DTO 结构的定义。每个功能应有一个文件。
  • http
    • http/controller 目录包含控制器包。每个功能应有其自己的包。例如,如果您有一个处理用户注册、用户档案等的控制器,您应该创建一个 http/controller/user 包。
    • 控制器包通常包含两个文件:
      • <feature>.go:包含控制器实现
      • validation.go:包含此功能的验证规则
    • http/middleware 目录包含应用程序中间件。每个中间件应有其自己的文件。
    • http/route 目录包含 route.go 中的主路由注册函数。此文件通常不会变得太大,因为路由可以从控制器自身注册。
    • http/validation 目录包含自定义验证器。
  • resources:资源目录用于存储静态资源,如图像和语言文件。此目录不应用作动态内容(如用户个人资料图片)的存储。
    • resources/lang 目录包含应用程序支持的语言和翻译。每种语言都有其自己的目录,应使用 ISO 639-1 语言代码命名。您还可以向语言添加变体:en-USen-UKfr-FRfr-CA、...大小写很重要。
    • 每种语言目录包含三个文件。每个文件是可选的
      • fields.json:字段名称翻译和字段特定规则消息。
      • locale.json:所有其他语言行。
      • rules.json:验证规则消息。
  • service:包含服务实现。
    • 每个服务应有其自己的包。
    • service/service.go 文件包含所有服务名称的常量列表。