后端开发指南
背景
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
中,特别是那些依赖于 models 的模組,因為它们依赖于数据库。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_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 文件中被导入时,很难知道我们使用的是哪个包以及它是变量名還是导入名。因此,我们始终建议使用导入别名。為了与常见的驼峰命名法的包变量区分开,建议使用snake_case作為导入别名的命名規則。
例如:import user_service "code.gitea.io/gitea/services/user"
重要注意事项
- 永远不要写成
x.Update(exemplar)
,而没有明确的WHERE
子句:- 这将导致表中的所有行都被使用 exemplar 的非零值進行更新,包括 ID。
- 通常應該写成
x.ID(id).Update(exemplar)
。
- 如果在迁移過程中使用
x.Insert(exemplar)
向表中插入记录,而 ID 是预设的:- 對於 MSSQL 变體,你将需要執行
SET IDENTITY_INSERT `table` ON
(否则迁移将失败) - 對於 PostgreSQL,你還需要更新 ID 序列,否则迁移将悄無声息地通過,但后续的插入将失败:
SELECT setval('table_name_id_seq', COALESCE((SELECT MAX(id)+1 FROM `table_name`), 1), false)
- 對於 MSSQL 变體,你将需要執行
未来的任务
目前,我们正在進行一些重构,以完成以下任务:
- 纠正不符合規則的代码。
models
中的文件太多了,所以我们正在将其中的一些移动到子包models/xxx
中。- 由于它们依赖于
models
,因此應将某些modules
子包移动到services
中。