iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
Software Development

Go Clean Architecture API 開發全攻略系列 第 14

Controller 層:API 的守門員

  • 分享至 

  • xImage
  •  

在前幾章,我們已經實作了 UseCase 層以及其外部依賴,現在我們要來說明 Controller 層的職責,並實作先前遺留下尚未實作的 Register function。

Controller 層的角色

在 Ports and Adapters 架構中,Controller 屬於 Adapter 的一種,它專門負責轉接外部的傳入請求 (in-coming/driving adapter)。以我們專案的 Web API 為例,Controller 層的主要職責包括:

  • 解析請求 (Parsing):從 HTTP Request 中解析出傳入的參數、路徑、標頭和 Body。
  • 驗證請求 (Validation):驗證傳入的資料是否符合格式與商業規則。
  • 調用 UseCase (Calling):根據請求內容,組裝 UseCase 需要的 Input 資料,並呼叫對應的 UseCase 來執行核心業務邏輯。
  • 處理結果 (Handling):處理 UseCase 回傳的 Outputerror
  • 回傳響應 (Responding):將結果轉換為外部世界看得懂的格式,例如 JSON,並回傳適當的 HTTP 狀態碼。

實作 Register Function

讓我們先從 request 資料結構開始。

請求驗證 (Request Validation)

我們使用 gin.ShouldBindJSON() 來自動解析 JSON Body 並進行驗證。驗證規則直接定義在 request struct 的 binding tag 中。

// internal/controller/request/user.go

type Register struct {
	Email    string `json:"email" binding:"required,email"`
	Password string `json:"password" binding:"required,min=8,max=32"`
}
  • json:"email":定義了 JSON Body 中的 email 欄位會對應到這個 struct field。
  • binding:"required,email"
    • required:表示這個欄位是必填的。
    • email:表示這個欄位的值必須是合法的 Email 格式。
  • binding:"required,min=8,max=32":表示密碼是必填的,且長度必須介於 8 到 32 個字元之間。

如果傳入的請求不符合這些規則,ShouldBindJSON() 會自動回傳一個錯誤,我們就可以直接回傳 400 Bad Request

Controller 實作 (含錯誤處理)

根據 Controller 的職責,並加上更細緻的錯誤處理,我們可以將 Register function 實作成以下版本:

// internal/controller/user.go

import (
	"errors"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/nick6969/go-clean-project/internal/controller/request"
	"github.com/nick6969/go-clean-project/internal/usecase/api/user/register"
)

// ... UserController struct ...

func (u *UserController) Register(usecase *register.UseCase) gin.HandlerFunc {
	return func(c *gin.Context) {
		var req request.Register
		// 1. 解析與驗證請求
		if err := c.ShouldBindJSON(&req); err != nil {
			// 若驗證失敗,回傳 400 狀態碼及錯誤訊息
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		// 2. 準備調用 UseCase 所需的資料
		input := register.NewInput(req.Email, req.Password)

		// 3. 調用 UseCase 的 Execute 方法
		output, err := usecase.Execute(c, input)

		// 4. 處理 UseCase 返回的結果
		if err != nil {
			// 專門處理已知的業務邏輯錯誤
			if errors.Is(err, errors.New("email is already registered")) {
				c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
				return
			}

			// 對於其他未知的內部錯誤,回傳 500
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}

		// 5. 成功,回傳 200 狀態碼及 token
		c.JSON(http.StatusOK, gin.H{"data": output.AccessToken})
	}
}

在這個版本中,我們特別處理了 UseCase 可能回傳的 email is already registered 錯誤。這是一個已知的業務邏輯錯誤,因此我們回傳 409 Conflict 狀態碼,讓 Client 端可以明確知道是 Email 衝突,而不是未知的伺服器錯誤。

思考點:標準化 API 回應

目前我們使用 gin.H 來建立 JSON 回應,雖然方便,但在大型專案中,建議定義標準的 API 回應 struct,例如:

// for success response
type APIGeneralSuccessModel struct {
  Data    interface{} `json:"data,omitempty"`
}

// for error response
type APIGeneralErrorModel struct {
  Code    string `json:"code"`
  Message string `json:"message"`
  Details string `json:"details,omitempty"`
}

這樣可以讓所有 API 的回應格式保持一致,方便 Client 端統一處理。

依賴關係的串連 (Wiring up Dependencies)

你可能會好奇,Register function 裡的 usecase 參數是從哪裡來的?這就是依賴注入的實踐。真正的串連發生在 http 層。

// internal/http/handle_api.go

func registerAPIUserRoutes(r gin.IRouter, app *application.Application) {
	uc := controller.NewUserController()
	// 將 app.UseCase.User.Register 這個已建立好的 UseCase 實例,
	// 作為參數傳遞給 uc.Register 方法。
	r.POST("register", uc.Register(app.UseCase.User.Register))
}

uc.Register() 是一個高階函數 (Higher-Order Function),它接收 UseCase 作為參數,並回傳一個 gin.HandlerFunc。這樣的設計有幾個好處:

  1. 關注點分離:Controller 完全不需要知道 UseCase 是如何被建立和初始化的。
  2. 可測試性:在單元測試中,我們可以輕易地傳入一個模擬 (mock) 的 UseCase,專心測試 Controller 的邏輯是否正確。
  3. 符合依賴注入原則:讓程式碼更具彈性、鬆耦合且可維護。

總結

在這一章,我們深入探討了 Controller 層的角色和職責,並透過一個包含細緻錯誤處理的 Register function 範例,展示了如何在實務中應用這些概念。我們也看到了如何透過 binding tag 進行宣告式的請求驗證,以及依賴注入是如何將各個層級優雅地串連起來。

現在,我們的 API 已經具備了接收請求、驗證輸入、執行業務邏輯並回傳結果的完整流程。

以上程式碼的完整內容可以到 Github 觀看


上一篇
Entity Model 與實作 Database Adapter
下一篇
六角架構的魅力之美:開發的便利
系列文
Go Clean Architecture API 開發全攻略19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言