在前幾章,我們已經實作了 UseCase 層以及其外部依賴,現在我們要來說明 Controller 層的職責,並實作先前遺留下尚未實作的 Register
function。
在 Ports and Adapters 架構中,Controller 屬於 Adapter 的一種,它專門負責轉接外部的傳入請求 (in-coming/driving adapter)。以我們專案的 Web API 為例,Controller 層的主要職責包括:
Input
資料,並呼叫對應的 UseCase 來執行核心業務邏輯。Output
或 error
。讓我們先從 request
資料結構開始。
我們使用 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 的職責,並加上更細緻的錯誤處理,我們可以將 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 端統一處理。
你可能會好奇,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
。這樣的設計有幾個好處:
在這一章,我們深入探討了 Controller 層的角色和職責,並透過一個包含細緻錯誤處理的 Register
function 範例,展示了如何在實務中應用這些概念。我們也看到了如何透過 binding
tag 進行宣告式的請求驗證,以及依賴注入是如何將各個層級優雅地串連起來。
現在,我們的 API 已經具備了接收請求、驗證輸入、執行業務邏輯並回傳結果的完整流程。
以上程式碼的完整內容可以到 Github 觀看