当前位置: 首页 > article >正文

18 | 实现简洁架构的 Handler 层

提示:

  • 所有体系课见专栏:Go 项目开发极速入门实战课;
  • 欢迎加入 云原生 AI 实战 星球,12+ 高质量体系课、20+ 高质量实战项目助你在 AI 时代建立技术竞争力(聚焦于 Go、云原生、AI Infra);
  • 本节课最终源码位于 fastgo 项目的 feature/s14 分支;
  • 更详细的课程版本见:Go 项目开发中级实战课:27 | 业务实现(4):实现 Handler 层代码

fastgo 三层简洁架构开发的最后一步便是开发 Handler 层代码。Handler 层代码的实现思路和 Biz 层、Store 层保持一致。

Handler 实现

要实现 Handler,主要分为以下几步:

  1. 实现创建 Handler 层实例的方法;
  2. 实现用户相关 Handler 方法;
  3. 对请求参数进行校验;
  4. 初始化 Handler。

步骤 1:实现创建 Handler 层实例的方法

HTTP API 接口最终的逻辑是由 Handler 方法来实现的。所以,需要先实现 Handler 方法。

fastgo 项目的 Handler 层代码位于 internal/apiserver/handler/ 目录中。新建一个 Handler 结构体,该结构体包含了 fg-apiserver 的路由函数。代码位于 internal/apiserver/handler/handler.go 文件中,内容如下:

package handler

import (
    "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.CreateUserRequest
    if 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 username
    if 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 underscores
    usernameRegex := regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
    if !usernameRegex.MatchString(rq.Username) {
        return errors.New("Username can only contain letters, numbers, and underscores")
    }

    // Validate password
    if 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 email
    if 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 number
    if 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 apiserver

import (
    ...
    "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


http://www.kler.cn/a/583063.html

相关文章:

  • python str repr方法区别
  • 数据库原理4
  • 开源链动2+1模式AI智能名片S2B2C商城小程序在KOC参与门店做透中的应用探索
  • 本地部署资源聚合搜索神器 Jackett 并实现外部访问
  • 苹果“被盗设备保护”的取证意义
  • Haproxy配置入门
  • 在芯片设计的后端流程中,通过metal修timing是什么意思,怎么实施。举个timing违例说明一下
  • 详解 C++ 与 C 兼容的接口(如 extern “C“ 函数)
  • 【Academy】SQL 注入 ------ SQL injection
  • [023-01-40].第40节:组件应用 - OpenFeign与 Sentinel 集成实现fallback服务降级
  • Flutter——Android与Flutter混合开发详细教程
  • 学习Android Audio 焦点记录
  • scoop退回软件版本的方法
  • 【AIGC】计算机视觉-YOLO系列家族
  • 【lf中的git实战】
  • Rust语言基础知识详解【九】
  • 【redis】hash基本命令和内部编码
  • 《MySQL数据库从零搭建到高效管理|库的基本操作》
  • leetcode hot100 图论
  • Hive-基础入门