跳至主要内容
版本:1.23.6

後端開發指南

背景

Gitea 使用 Golang 作為後端程式語言。它使用了許多第三方套件,也自己寫了一些。 例如,Gitea 使用 Chi 作為基本的網頁框架。Xorm 是一個 ORM 框架,用來與資料庫互動。 因此,管理這些套件非常重要。在開始寫後端程式碼之前,請遵循以下指南。

套件設計指南

套件列表

為了保持程式碼的可理解性並避免循環依賴,擁有良好的程式碼結構非常重要。Gitea 的後端分為以下幾個部分:

  • build: 幫助構建 Gitea 的腳本。
  • cmd: 所有 Gitea 的實際子命令,包括 web、doctor、serv、hooks、admin 等等。web 會啟動網頁服務。servhooks 會被 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 只會被內部子命令調用,特別是 servhooks
    • routers/web 會處理來自網頁瀏覽器或 Git SMART HTTP 協議的 HTTP 請求。
  • services: 支持常見路由操作或命令執行的函數。使用 modelsmodules 來處理請求。
  • 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)

// ...
}

套件名稱

對於頂層套件,使用複數作為套件名稱,即 servicesmodels,對於子套件,使用單數, 即 services/usermodels/repository

導入別名

由於有些套件使用相同的套件名稱,可能會發現像 modules/usermodels/userservices/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)

未來任務

目前,我們正在進行一些重構,以完成以下事情:

  • 修正不符合規則的代碼。
  • models 中的文件太多了,所以我們正在將其中一些移動到子套件 models/xxx
  • 一些 modules 子套件應移動到 services,因為它們依賴於 models