首页
/ Gitea 后端开发规范详解:Go 包分层、数据库事务与 API v1 设计准则

Gitea 后端开发规范详解:Go 包分层、数据库事务与 API v1 设计准则

2026-09-05 20:28:52作者:何举烈Damon

本文基于 Gitea 官方后端开发指南 docs/guidelines-backend.md 展开,系统讲解 Gitea Go 后端的包结构分层、单向依赖约束、XORM 事务写法与 API v1 路由设计规范,并结合 models/dbrouters/api/v1 等真实源码逐一印证这些规范背后的实现机制。读完本文,你将掌握向 Gitea 贡献后端代码前必须理解的架构约定:如何正确组织包、如何编写可回滚的事务、如何新增一条符合 GitHub API 兼容性的 v1 接口。

技术栈总览

Gitea 的后端使用 Go 编写,两个核心基础设施如下:

  • Web 路由chi 框架处理,go.mod 中实际引入的是 github.com/go-chi/chi/v5 v5.3.1
  • 数据库访问通过 XORM ORM 完成,go.mod 中锁定 xorm.io/xorm v1.4.1 及其表达式构建器 xorm.io/builder v0.3.13

指南明确指出:在贡献任何后端代码之前,理解各包之间的依赖关系是前提。下面各节内容即按此主线展开。

包结构(Package layout)

后端被拆分为若干顶层包,每个包有明确的职责边界:

顶层包 职责
build 编译期使用的辅助脚本
cmd 子命令入口,如 webservhooksdoctor 及各类管理工具
models 数据结构与数据库操作(XORM),刻意保持外部依赖最小化
modelmigration 数据库 schema 迁移脚本
modules 独立功能模块,依赖很少
routers 请求处理器,细分为 apiwebinstallprivate
services 业务逻辑层,负责串联 routers 与 models
templates Go HTML 模板
public 编译后的前端资源
tests 集成测试与端到端测试辅助代码

其中两个子包值得单独注意:

  • models/db:核心数据库操作封装(事务、连接、游标等均在此);
  • models/fixtures:测试使用的样例数据;
  • modules/setting:配置处理;modules/git:与 Git 命令行的交互;
  • routers 内部按接口域划分:apiwebinstallprivate

这个划分在仓库中可以直接对照:例如 cmd/ 目录下是 web.goserv.godoctor.go 等子命令,routers/ 目录按 api/web/install/private/ 四个子目录组织,modelmigration/ 下按版本号组织迁移脚本(如 v1_26/v330.go)。

依赖方向:只允许单向流动

包之间的依赖只能沿一个方向流动:

cmd → routers → services → models → modules

规则是:左侧的包可以导入右侧的包,但绝不允许反向导入。这意味着:

  • routers 中的请求处理器可以调用 services 层的业务逻辑,但 services 绝不能反过来导入 routers
  • services 可以读写 models 层的数据,models 只能依赖最底层的 modules(以及 models/db 这类自身的基础设施);
  • modelsmodules 的依赖是"最小外部依赖"设计的一部分——数据层不应被上层框架污染。

这条约束是 review 时判断"这段代码该放哪一层"的核心依据:如果一个函数同时需要 HTTP 上下文和数据库会话,它通常属于 servicesrouters,而不该下沉到 models

命名约定与导入别名

  • 顶层包使用复数形式servicesmodelsrouters
  • 子包使用单数形式services/usermodels/repository

当不同层出现同名的包时,用 snake_case 导入别名消歧:

import user_service "gitea.dev/services/user"

这种写法让调用点(如 user_service.CreateUser(...))能一眼看出代码来自哪个层,避免与 models/user 混淆。

数据库事务:db.WithTxdb.WithTx2

指南要求:必须一起回滚的操作,必须运行在 db.WithTx()(需要返回值时用 db.WithTx2())内部。这两个函数定义在 models/db/context.go 中。参与事务的函数一律以 context.Context 作为第一个参数,以便把事务会话沿调用链传递下去。

源码实现印证

从源码看,WithTx 的核心逻辑在 models/db/context.go

// WithTx represents executing database operations on a transaction, if the transaction exist,
// this function will reuse it otherwise will create a new one and close it when finished.
func WithTx(parentCtx context.Context, f func(ctx context.Context) error) error {
	if sess := getTransactionSession(parentCtx); sess != nil {
		err := f(withContextEngine(parentCtx, sess))
		if err != nil {
			// rollback immediately, in case the caller ignores returned error and tries to commit the transaction.
			_ = sess.Close()
		}
		return err
	}
	return txWithNoCheck(parentCtx, f)
}

可以提炼出三个关键行为:

  1. 事务复用优先:若父上下文中已存在事务会话(getTransactionSession 非空),则直接复用,而不是嵌套新事务;
  2. 错误即回滚:回调执行出错时立即 sess.Close() 触发回滚,防止上层忽略错误后误提交;
  3. 自动提交:无既有事务时走 txWithNoCheck,回调无错则 sess.Commit()(见 models/db/context.go)。

WithTx2 是泛型版本(models/db/context.go),在 WithTx 基础上多返回一个业务值:

func WithTx2T any (T, error)) (ret T, errRet error)

典型使用场景是"在一个事务里插入多条记录并最终返回新建对象",例如创建仓库、创建 Issue 这类复合写操作。

旧式 TxContext 的兼容写法

对于维护仍使用旧式 TxContext(parentCtx) 接口的代码,models/db/context.go 的注释给出了必须遵守的纪律:

  • 返回前无论是否调用过 Commit(),都必须调用 Close()
  • 无错误时必须调用 Commit()(即使没有数据变更);
  • 复用父事务时返回的 CommitterhalfCommitter:此时 Commit() 是空操作,而未 Commit()Close()回滚整个调用栈上的所有操作,不只是当前函数的操作;
  • 回滚只应在确定出错且确实需要回滚时进行。

源码注释也建议:新代码一律使用 WithTx,不要再引入 TxContext 用法

XORM 使用陷阱(XORM gotchas)

指南列出了三条必须遵守的 XORM 使用红线:

  1. 禁止无 WHERE 的 x.Update(exemplar)——它会更新表中的每一行,属于高危全表误写操作;
  2. 部分表结构同步必须用 SyncWithOptions(IgnoreDrop...),而不是裸 Sync。这一点在源码中可以印证:models/db/engine.go 中的表同步正是通过 xormEngine.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{...}) 完成的,SyncOptions 允许显式声明"忽略删除列/索引"等语义,避免升级过程中误删用户数据;
  3. 预置主键插入的数据库差异:MSSQL 需要先开启 SET IDENTITY_INSERT,PostgreSQL 则需要在插入后更新序列(sequence),否则自增序列会与已插入的大 ID 冲突。

依赖管理:go.mod / go.sum 的修改纪律

Gitea 使用 Go Modules 管理依赖,指南对 go.mod / go.sum 的修改有严格纪律:

  • 一个 PR 只允许修改与本次变更直接相关的依赖(无论 bug fix 还是新功能);其余场景下,这两个文件只应由"唯一目的就是更新依赖"的 PR 修改;
  • 任何 go.mod 变更后必须运行 make tidy
  • 任何 go.mod / go.sum 更新都必须在 PR 描述中说明理由,并由 reviewer 与合并者共同核对引用的上游 commit 真实存在。

其中 make tidy 对应 Makefile 中的 tidy 目标(第 420 行起):

tidy: ## run go mod tidy
	$(eval MIN_GO_VERSION := $(shell grep -Eo '^go\s+[0-9]+\.[0-9.]+' go.mod | cut -d' ' -f2))
	$(eval GO_TOOLCHAIN := $(shell grep -Eo '^toolchain\s+go[0-9.]+' go.mod | cut -d' ' -f2))
	$(GO) mod tidy -compat=$(MIN_GO_VERSION)
	@# workaround https://github.com/golang/go/issues/75331: restore toolchain if tidy dropped it

可以看到它并非简单执行 go mod tidy,而是先从 go.mod 解析出最低 Go 版本,用 -compat=$(MIN_GO_VERSION) 保证清理结果与声明的工具链兼容,并对 tidy 误删 toolchain 声明的已知上游 bug 做了恢复处理。

Go HTML 模板规范

Gitea 使用 Go 内置的 HTML 模板引擎,它是动态且弱类型的。为了让模板代码可维护,指南给出六条规则:

  1. 复杂逻辑交给 Go 代码:Go 代码尽可能准备好模板数据,模板只负责渲染;

  2. 模板数据结构优先使用 Go struct,而不是 map

  3. 非局部变量避免使用单词名;

  4. 不要把 "root" $"." . 整个传给子模板,只传子模板真正需要的数据;

  5. 用显式变量名访问数据,而不是裸 .,例如:

    {{range $item := $.TargetItems}}{{ $item.Name }}{{end}}

  6. 渲染逻辑过于复杂时,用 Go 代码实现 render helper。

关于模板引擎替换,指南持审慎态度:用现代静态强类型模板引擎替换 Go HTML template 或许更好,但挑战巨大,前提是满足两点——针对最复杂模板(如 "PR View" 和 "PR Diff")做出可运行的 PoC,以及有足够工程资源的坚定投入。在此之前,现有约定是唯一标准。模板实体位于 templates 目录,按 admin/repo/user/shared/ 等域划分,与上述规范一一对应。

API v1 设计规范

Gitea 的 API 以 Swagger 文档化,并以 GitHub API 为模型进行设计。这一节是后端贡献者新增/修改接口时必须遵守的完整契约。

GitHub API 兼容性原则

Gitea API 在可能的情况下应与 GitHub API 使用相同的端点和字段,除非有充分理由偏离。具体规则:

  • Gitea 提供了 GitHub 没有的功能时,可以新增端点
  • Gitea 暴露了 GitHub API 没有的信息时,可以新增字段,前提是不与 GitHub 字段冲突;
  • 既有字段不得删除(有强理由除外),状态码响应同理。

如果发现了必须通过破坏性变更才能解决的问题,正确做法是在代码中留注释,标记为未来 API v2 的重构项(v2 目前并未计划),而不是直接破坏 v1。

新增与维护 API 路由

三条硬性要求:

  • 路由的 swagger 注释中必须记录所有可能的结果(错误、成功、失败消息);
  • 每个 JSON 请求体必须定义为 modules/structs/ 中的 struct,并注册到 routers/api/v1/swagger/options.go
  • 每个 JSON 响应也必须定义为 modules/structs/ 中的 struct,并按类别注册到 routers/api/v1/swagger/ 目录下(该目录包含 repo.gouser.goorg.goissue.goactivity.go 等按资源域划分的注册文件)。

HTTP 方法与状态码约定

方法 语义 状态码与响应体
GET 返回请求的对象 200 OK
POST 创建新对象(如用户) 201 Created + 创建的对象
PUT 添加或指派已有对象(如把用户加入团队) 204 No Content,无响应体
PATCH 编辑已有对象 200 OK + 变更后的对象
DELETE 删除对象 204 No Content,无响应体

API 路由的通用要求

  • 编辑对象的端点,其所有参数都必须是可选的,唯一定位对象所必需的那些参数除外(这些是必填的);
  • 返回列表的端点必须支持分页pagelimit 查询参数),并通过 ctx.SetTotalCountHeader(...) 设置 X-Total-Count 响应头。

在源码中可以验证第二条要求被普遍执行:routers/api/v1/ 下大量路由(如 admin/adopt.goadmin/cron.goadmin/hooks.go)都调用了 ctx.SetTotalCountHeader(count),该计数头是 Gitea 客户端展示分页总页数的依据。

小结:贡献后端代码前的自检清单

  • 新代码放对了层吗?依赖是否只沿 cmd → routers → services → models → modules 单向流动?
  • 复合写操作是否包裹在 db.WithTx() / db.WithTx2() 中?参与函数是否以 context.Context 为首参?
  • 是否触碰了 XORM 三条红线(无 WHERE 的 Update、裸 Sync、预置 ID 插入)?
  • go.mod 的改动是否与本次 PR 直接相关?是否运行了 make tidy
  • 新增 API 是否对齐 GitHub 端点/字段、注册了 swagger 注释与 modules/structs 结构、支持分页并设置了 X-Total-Count

以上约定与 CONTRIBUTING.md 的通用贡献流程、docs/development.md 的构建说明以及 docs/testing.md 的测试规范配套使用,共同构成 Gitea 后端开发的完整行为准则。

登录后查看全文
热门项目推荐
相关项目推荐