后端开发指南
背景
Gitea 使用 Golang 作为后端编程语言。它使用了许多第三方包,也编写了一些自己的包。例如,Gitea 使用 Chi 作为基本的 Web 框架。 Xorm 是一个 ORM 框架,用于与数据库交互。因此,管理这些包非常重要。在开始编写后端代码之前,请参考以下指南。
包设计指南
包列表
为了维护易于理解的代码并避免循环依赖,良好的代码结构至关重要。Gitea 后端被划分为以下几个部分
build
: 构建 Gitea 的脚本。cmd
: 所有 Gitea 的实际子命令,包括 web、doctor、serv、hooks、admin 等。web
将启动 Web 服务。serv
和hooks
将由 Git 或 OpenSSH 调用。其他子命令可以帮助维护 Gitea。tests
: 常用的测试实用程序函数tests/integration
: 集成测试,用于测试后端回归。tests/e2e
: 端到端测试,用于测试前端和后端兼容性以及视觉回归。
models
: 包含 xorm 用于构建数据库表的的数据结构。它还包含查询和更新数据库的函数。应避免对其他 Gitea 代码的依赖。在某些情况下,例如日志记录,您可以做出例外。models/db
: 基本的数据库操作。所有其他models/xxx
包都应依赖于此包。GetEngine
函数只能从models/
中调用。models/fixtures
: 单元测试和集成测试中使用的示例数据。一个yml
文件表示一个表,该表将在测试开始时加载到数据库中。models/migrations
: 存储不同版本之间的数据库迁移。更改数据库结构的 PR **必须**也包含迁移步骤。
modules
: 处理 Gitea 中特定功能的不同模块。正在进行中:其中一些应该移动到services
,特别是那些依赖于模型的模块,因为它们依赖于数据库。modules/setting
: 存储从 ini 文件读取的所有系统配置,并被各个地方引用。但应尽可能将其用作函数参数。modules/git
: 与Git
命令行或 Gogit 包交互的包。
public
: 编译后的前端文件(javascript、图像、css 等)。routers
: 处理服务器请求。由于它使用其他 Gitea 包来提供服务请求,因此其他包(models、modules 或 services)不得依赖于 routers。routers/api
包含/api/v1
的路由器,旨在处理 RESTful API 请求。routers/install
仅在系统处于 INSTALL 模式(INSTALL_LOCK=false)时才能响应。routers/private
仅由内部子命令调用,特别是serv
和hooks
。routers/web
将处理来自 Web 浏览器或 Git SMART HTTP 协议的 HTTP 请求。
services
: 支持常见的路由操作或命令执行的函数。使用models
和modules
处理请求。templates
: 用于生成 html 输出的 Golang 模板。
包依赖
由于 Golang 不支持导入循环,因此我们必须仔细确定包依赖关系。这些包之间存在一些层次关系。以下是理想的包依赖方向。
cmd
-> routers
-> services
-> models
-> modules
从左到右,左侧包可以依赖于右侧包,但右侧包**不得**依赖于左侧包。同一级别的子包可以根据该级别的规则进行依赖。
为什么我们需要在 models
之外进行数据库事务?以及如何进行?某些操作在数据库记录插入/更新/删除失败时应允许回滚。因此,服务必须允许创建数据库事务。以下是一些示例,
// services/repository/repository.go
func CreateXXXX() error {
return db.WithTx(func(ctx context.Context) error {
// do something, if err is returned, it will rollback automatically
if err := issues.UpdateIssue(ctx, repoID); err != nil {
// ...
return err
}
// ...
return nil
})
}
您**不应该**直接在 services
中使用 db.GetEngine(ctx)
,而应该在 models/
下编写一个函数。如果该函数将在事务中使用,只需将 context.Context
作为函数的第一个参数。
// models/issues/issue.go
func UpdateIssue(ctx context.Context, repoID int64) error {
e := db.GetEngine(ctx)
// ...
}
包名
对于顶级包,使用复数作为包名,例如 services
、models
;对于子包,使用单数,例如 services/user
、models/repository
。
导入别名
由于某些包使用相同的包名,因此您可能会发现诸如 modules/user
、models/user
和 services/user
之类的包。当这些包在一个 Go 文件中导入时,很难知道我们正在使用哪个包,以及它是一个变量名还是一个导入名。因此,我们始终建议使用导入别名。为了与通常以 camelCase 表示的包变量区分开来,只需对导入别名使用**snake_case**。例如 import user_service "code.gitea.io/gitea/services/user"
实现 io.Closer
如果某个类型实现了 io.Closer
,则多次调用 Close
必须不会失败或出现 panic
,而是返回错误或 nil
。
重要注意事项
- 切勿在没有显式
WHERE
子句的情况下编写x.Update(exemplar)
- 这将导致表中的所有行都使用 exemplar 的非零值进行更新 - 包括 ID。
- 您通常应该编写
x.ID(id).Update(exemplar)
。
- 如果在迁移过程中,您使用
x.Insert(exemplar)
向表中插入数据,其中 ID 已预设- 对于 MSSQL 变体,您需要
SET IDENTITY_INSERT `table` ON
(否则迁移将失败) - 但是,您还需要更新 postgres 的 id 序列 - 迁移在这里将静默通过,但后续插入将失败:
SELECT setval('table_name_id_seq', COALESCE((SELECT MAX(id)+1 FROM `table_name`), 1), false)
- 对于 MSSQL 变体,您需要
未来任务
目前,我们正在进行一些重构,以执行以下操作
- 修正不符合规则的代码。
models
中的文件过多,因此我们正在将其中一些文件移动到子包models/xxx
中。- 一些
modules
子包应该移动到services
,因为它们依赖于models
。