提示:
- 所有體系課見專欄:Go 項目開發極速入門實戰課;
- 歡迎加入我的訓練營:云原生AI實戰營,一個助力 Go 開發者在 AI 時代建立技術競爭力的實戰營;
- 本節課最終源碼位于 fastgo 項目的 feature/s14 分支;
- 更詳細的課程版本見:Go 項目開發中級實戰課:27 | 業務實現(4):實現 Handler 層代碼
fastgo 三層簡潔架構開發的最后一步便是開發 Handler 層代碼。Handler 層代碼的實現思路和 Biz 層、Store 層保持一致。
Handler 實現
要實現 Handler,主要分為以下幾步:
- 實現創建 Handler 層實例的方法;
- 實現用戶相關 Handler 方法;
- 對請求參數進行校驗;
- 初始化 Handler。
步驟 1:實現創建 Handler 層實例的方法
HTTP API 接口最終的邏輯是由 Handler 方法來實現的。所以,需要先實現 Handler 方法。
fastgo 項目的 Handler 層代碼位于 internal/apiserver/handler/ 目錄中。新建一個 Handler 結構體,該結構體包含了 fg-apiserver 的路由函數。代碼位于 internal/apiserver/handler/handler.go 文件中,內容如下:
package handlerimport ("github.com/onexstack/fastgo/internal/apiserver/biz"
)// Handler 處理博客模塊的請求.
type Handler struct {biz biz.IBiz
}// NewHandler 創建新的 Handler 實例.
func NewHandler(biz biz.IBiz) *Handler {return &Handler{biz: biz,}
}
Handler 結構體中包含了 Biz 層的 IBiz 接口,IBiz 接口中包含的方法用來執行具體的業務邏輯。:
步驟 2: 實現用戶相關 Handler 方法
internal/apiserver/handler/user.go 文件中包含了用戶相關的 Handler 方法。這些 Handler 方法的實現邏輯保持一致。實現邏輯如下:
這里,我介紹下 CreateUser 路由方法的實現,其他路由實現方法類似。CreateUser
路由方法代碼如下:
// CreateUser 創建新用戶.
func (h *Handler) CreateUser(c *gin.Context) {slog.Info("Create user function called")var rq v1.CreateUserRequestif err := c.ShouldBindJSON(&rq); err != nil {core.WriteResponse(c, errorsx.ErrBind, nil)return}if err := validation.ValidateCreateUserRequest(c.Request.Context(), &rq); err != nil {core.WriteResponse(c, errorsx.ErrInvalidArgument.WithMessage(err.Error()), nil)return}resp, err := h.biz.UserV1().Create(c.Request.Context(), &rq)if err != nil {core.WriteResponse(c, err, nil)return}core.WriteResponse(c, nil, resp)
}
首先調用 c.ShouldBindJSON
方法將請求中的參數解析到 v1.CreateUserRequest
類型的變量 rq
中。如果解析失敗,返回 errorsx.ErrBind
類型的自定義錯誤。
接著,調用 validation.ValidateCreateUserRequest
函數,對請求參數進行校驗。為了統一管理請求參數的校驗方法,提高代碼可維護性。將校驗方法統一放在 validation
(位于 internal/apiserver/pkg/validation 目錄中)。如果校驗失敗,返回 errorsx.ErrInvalidArgument
類型的自定義錯誤。這里要注意,傳遞的 context 是 c.Request.Context()
,而不是 *gin.Context
類型的變量 c
。因為 c
中缺少了一些 HTTP 請求上下文信息。
接著,調用 Biz 層的方法 h.biz.UserV1().Create
執行具體的業務邏輯。
gin.Context
結構體類型提供了以下方法,分別用來綁定不同位置的請求參數到結構體:
ShouldBindJSON
:ShouldBindUri
:將請求中的路徑參數綁定到 Go 結構體中的對應字段上,這些字段跟路徑參數的映射關系,是通過 Go 結構體字段的uri
標簽來映射的;- XXXX:
步驟 3:對請求參數進行校驗
首先創建一個校驗類型的結構體 Validator,代碼位于 internal/apiserver/pkg/validation/validation.go 文件中,內容如下:
// Validator 是驗證邏輯的實現結構體.
type Validator struct {// 有些復雜的驗證邏輯,可能需要直接查詢數據庫// 這里只是一個舉例,如果驗證時,有其他依賴的客戶端/服務/資源等,// 都可以一并注入進來store store.IStore
}// NewValidator 創建一個新的 Validator 實例.
func NewValidator(store store.IStore) *Validator {return &Validator{store: store}
}
在 Validator
結構體中,可以添加校驗邏輯中依賴的依賴項。例如 store.IStore
類型的實例,第三方微服務客戶端等。以此實現更加復雜的校驗邏輯。
ValidateCreateUserRequest 方法實現如下:
func (v *Validator) ValidateCreateUserRequest(ctx context.Context, rq *v1.CreateUserRequest) error {// Validate usernameif rq.Username == "" {return errors.New("Username cannot be empty")}if len(rq.Username) < 4 || len(rq.Username) > 32 {return errors.New("Username must be between 4 and 32 characters")}// Username can only contain letters, numbers, and underscoresusernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_]+$`)if !usernameRegex.MatchString(rq.Username) {return errors.New("Username can only contain letters, numbers, and underscores")}// Validate passwordif rq.Password == "" {return errors.New("Password cannot be empty")}if len(rq.Password) < 8 || len(rq.Password) > 64 {return errors.New("Password must be between 8 and 64 characters")}// Validate password complexity (must contain at least one letter and one number)passwordRegex := regexp.MustCompile(`^.*(?=.*[a-zA-Z])(?=.*\d).*$`)if !passwordRegex.MatchString(rq.Password) {return errors.New("Password must contain at least one letter and one number")}// Validate nickname (if provided)if rq.Nickname != nil && *rq.Nickname != "" {if len(*rq.Nickname) > 32 {return errors.New("Nickname cannot exceed 32 characters")}}// Validate emailif rq.Email == "" {return errors.New("Email cannot be empty")}emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)if !emailRegex.MatchString(rq.Email) {return errors.New("Invalid email format")}// Validate phone numberif rq.Phone == "" {return errors.New("Phone number cannot be empty")}// Validate Chinese mainland phone number format (11 digits starting with 1)phoneRegex := regexp.MustCompile(`^1\d{10}$`)if !phoneRegex.MatchString(rq.Phone) {return errors.New("Invalid phone number format, must be 11 digits starting with 1")}return nil
}
上述校驗邏輯代碼比較簡單,這里不再介紹。為了提高代碼的可維護性,將用戶相關的校驗方法統一保存在 internal/apiserver/pkg/validation/user.go 文件中。user.go 文件中只實現了 v1.CreateUserRequest
請求結構體的校驗邏輯。
v1.UpdateUserRequest
等其他請求結構體的校驗代碼實現,留個作業,由你來實現。
步驟 4:初始化 Handler
修改 internal/apiserver/server.go 文件,添加以下代碼:
package apiserverimport (..."github.com/onexstack/fastgo/internal/apiserver/biz""github.com/onexstack/fastgo/internal/apiserver/handler""github.com/onexstack/fastgo/internal/apiserver/pkg/validation""github.com/onexstack/fastgo/internal/apiserver/store"...
)
...
// NewServer 根據配置創建服務器.
func (cfg *Config) NewServer() (*Server, error) {...// 初始化數據庫連接db, err := cfg.MySQLOptions.NewDB()if err != nil {return nil, err}store := store.NewStore(db)cfg.InstallRESTAPI(engine, store)...
}
在 NewServer 方法中,通過調用 cfg.MySQLOptions.NewDB()
創建了一個 *gorm.DB
的實例 db
,再使用 db 創建了 store.IStore
的實例 store
。
將路由安裝代碼在 cfg.InstallRESTAPI 方法中實現,這樣可以使 NewServer
更加簡潔,同時也便于統一維護路由設置。
cfg.InstallRESTAPI
方法實現如下:
// 注冊 API 路由。路由的路徑和 HTTP 方法,嚴格遵循 REST 規范.
func (cfg *Config) InstallRESTAPI(engine *gin.Engine, store store.IStore) {...// 創建核心業務處理器handler := handler.NewHandler(biz.NewBiz(store), validation.NewValidator(store))authMiddlewares := []gin.HandlerFunc{}// 注冊 v1 版本 API 路由分組v1 := engine.Group("/v1"){// 用戶相關路由userv1 := v1.Group("/users"){// 創建用戶。這里要注意:創建用戶是不用進行認證和授權的userv1.POST("", handler.CreateUser)userv1.PUT(":userID", handler.UpdateUser) // 更新用戶信息userv1.DELETE(":userID", handler.DeleteUser) // 刪除用戶userv1.GET(":userID", handler.GetUser) // 查詢用戶詳情userv1.GET("", handler.ListUser) // 查詢用戶列表.}// 博客相關路由postv1 := v1.Group("/posts", authMiddlewares...){postv1.POST("", handler.CreatePost) // 創建博客postv1.PUT(":postID", handler.UpdatePost) // 更新博客postv1.DELETE("", handler.DeletePost) // 刪除博客postv1.GET(":postID", handler.GetPost) // 查詢博客詳情postv1.GET("", handler.ListPost) // 查詢博客列表}}
}
在 InstallRESTAPI
方法中,通過 handler.NewHandler
函數創建了 Handler 層的實例。并使用 Handler 的實例 handler
提供的路由方法來設置 HTTP 路由。
創建 handler
實例依賴 biz.IBiz
、*validation.Validator
類型的實例。上述實例分別通過 biz.NewBiz(store)
、validation.NewValidator(store)
函數來創建。
上述代碼,使用 Gin 框架提供的各類路由注冊方法注冊了符合 REST 規范的 HTTP 路由。Gin 框架如何注冊路由,請閱讀 Gin GitHub 項目倉庫的 README 文件。上述代碼注冊的 HTTP 路由見 下表所示。
HTTP 路由(HTTP 方法 HTTP 路徑) | 路由描述 |
---|---|
GET /healthz | 健康檢查接口 |
POST /v1/users | 創建用戶 |
PUT /v1/users/:userID | 更新用戶信息 |
DELETE /v1/users/:userID | 刪除用戶 |
GET /v1/users/:userID | 獲取用戶信息 |
GET /v1/users | 列出所有用戶 |
POST /v1/posts | 創建文章 |
PUT /v1/posts/:postID | 更新文章 |
DELETE /v1/posts | 刪除文章 |
GET /v1/posts/:postID | 獲取文章信息 |
GET /v1/posts | 列出所有文章 |
編譯并測試
執行以下命令重新編譯并運行 fg-apiserver:
$ ./build.sh
$ _output/fg-apiserver -c configs/fg-apiserver.yaml
打開另一個 Linux 終端,執行以下命令測試 HTTP 接口是否正常工作:
$ curl -XPOST -H'Content-Type: application/json' http://127.0.0.1:6666/v1/users -d '{"username":"colin","password":"fastgo1234","nickname":"belm","email":"nosbelm@qq.com","phone":"1818888xxxx"}'
{"userID":"user-gxqfqn"}
上述命令創建了一個新的用戶,并返回了 用戶 ID user-gxqfqn
。