後端開發指南
背景
Gitea 使用 Golang 作為後端程式語言。它使用了許多第三方套件,也自己寫了一些。 例如,Gitea 使用 Chi 作為基本的網頁框架。Xorm 是一個 ORM 框架,用來與資料庫互動。 因此,管理這些套件非常重要。在開始寫後端程式碼之前,請遵循以下指南。
套件設計指南
套件列表
為了保持程式碼的可理解性並避免循環依賴,擁有良好的程式碼結構非常重要。Gitea 的後端分為以下幾個部分:
build
: 幫助構建 Gitea 的腳本。cmd
: 所有 Gitea 的實際子命令,包括 web、doctor、serv、hooks、admin 等等。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
包含處理 RESTful API 請求的/api/v1
路由。routers/install
僅在系統處於安裝模式(INSTALL_LOCK=false)時響應。routers/private
只會被內部子命令調用,特別是serv
和hooks
。routers/web
會處理來自網頁瀏覽器或 Git SMART HTTP 協議的 HTTP 請求。
services
: 支持常見路由操作或命令執行的函數。使用models
和modules
來處理請求。templates
: 用於生成 html 輸出的 Golang 模板。
套件依賴
由於 Golang 不支持導入循環,我們必須仔細決定套件依賴關係。這些套件之間有一些層次。以下是理想的套件依賴方向。
cmd
-> routers
-> services
-> models
-> modules
從左到右,左邊的套件可以依賴右邊的套件,但右邊的套件不得依賴左邊的套件。同一層次的子套件可以根據該層次的規則依賴。
為什麼我們需要在 models
之外的資料庫交易?以及如何實現?
某些操作應允許在資料庫記錄插入/更新/刪除失敗時回滾。
因此,services 必須允許創建資料庫交易。這裡有一些例子,
// services/repository/repository.go
func CreateXXXX() error {
return db.WithTx(func(ctx context.Context) error {
// 做一些事情,如果返回錯誤,它會自動回滾
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"
實現 io.Closer
如果一種類型實現了 io.Closer
,多次調用 Close
不應失敗或 panic
,而是返回錯誤或 nil
。
重要注意事項
- 永遠不要在沒有明確
WHERE
子句的情況下寫x.Update(exemplar)
:- 這會導致表中的所有行都被更新為 exemplar 的非零值 - 包括 ID。
- 你通常應該寫
x.ID(id).Update(exemplar)
。
- 如果在遷移期間你正在插入一個預設 ID 的表格,使用
x.Insert(exemplar)
:- 對於 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
。