---
date: "2021-11-01T23:41:00+08:00"
slug: "guidelines-backend"
sidebar_position: 20
aliases:
  - /zh-tw/guidelines-backend
---

# 後端開發指南

## 背景

Gitea 使用 Golang 作為後端程式語言。它使用了許多第三方套件，也自己寫了一些。
例如，Gitea 使用 [Chi](https://github.com/go-chi/chi) 作為基本的網頁框架。[Xorm](https://xorm.io) 是一個 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`

從左到右，左邊的套件可以依賴右邊的套件，但右邊的套件不得依賴左邊的套件。同一層次的子套件可以根據該層次的規則依賴。

:::warning
為什麼我們需要在 `models` 之外的資料庫交易？以及如何實現？
某些操作應允許在資料庫記錄插入/更新/刪除失敗時回滾。
因此，services 必須允許創建資料庫交易。這裡有一些例子，

```go
// 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` 作為該函數的第一個參數。

```go
// 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) ``

### 未來任務

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

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