18 | 实现简洁架构的 Handler 层

提示:

  • 所有体系课见专栏:Go 项目开发极速入门实战课;
  • 欢迎加入我的训练营:云原生AI实战营,一个助力 Go 开发者在 AI 时代建立技术竞争力的实战营;
  • 本节课最终源码位于 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 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/897593.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝队第三次

1.了解什么是盲注 盲注&#xff08;Blind SQL Injection&#xff09;是SQL注入的一种形式&#xff0c;攻击者无法直接通过页面回显或错误信息获取数据&#xff0c;而是通过观察页面的布尔状态&#xff08;真/假&#xff09;或时间延迟来间接推断数据库信息。例如&#xff0c;通…

sql server 2016 版本补丁说明

包信息和发布类型 Microsoft为创建和分发的 SQL Server 的所有软件更新包采用了标准化命名架构。 软件更新包是一个可执行文件&#xff08;.exe 或 .msi&#xff09;文件&#xff0c;其中包含一个或多个文件&#xff0c;这些文件可能应用于 SQL Server 安装以更正特定问题。 …

STM32之I2C硬件外设

注意&#xff1a;硬件I2C的引脚是固定的 SDA和SCL都是复用到外部引脚。 SDA发送时数据寄存器的数据在数据移位寄存器空闲的状态下进入数据移位寄存器&#xff0c;此时会置状态寄存器的TXE为1&#xff0c;表示发送寄存器为空&#xff0c;然后往数据控制寄存器中一位一位的移送数…

从青铜到王者:六大排序算法实战解析

前言 在编程的世界里,排序算法如同一颗璀璨的明珠,闪耀着智慧的光芒。它不仅是计算机科学的基础知识点,更是每一位程序员必备的技能。今天,就让我们一同走进排序算法的世界,深入探究冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序这六大经典算法的精髓所在,…

小程序配置webview

1.在微信公众平台配置业务域名 1&#xff09;包括把校验文件放在服务器根目录 2&#xff09;配置域名 2.在小程序中 新建文件 小程序新建页面&#xff1a;web-view json配置&#xff1a;{ "pageOrientation": "landscape", "renderer":&qu…

不用 Tomcat?SpringBoot 项目用啥代替?

在SpringBoot框架中&#xff0c;我们使用最多的是Tomcat&#xff0c;这是SpringBoot默认的容器技术&#xff0c;而且是内嵌式的Tomcat。 同时&#xff0c;SpringBoot也支持Undertow容器&#xff0c;我们可以很方便的用Undertow替换Tomcat&#xff0c;而Undertow的性能和内存使…

线索二叉树构造及遍历算法

线索二叉树构造以及遍历算法 线索二叉树&#xff08;中序遍历版&#xff09;构造线索二叉树构造双向线索链表遍历中序线索二叉树 线索二叉树&#xff08;中序遍历版&#xff09; 中序遍历找到对应结点的前驱&#xff08;土方法&#xff09; #mermaid-svg-eunGO5d2GhjLxCn5 {fo…

基于SpringBoot的“体育购物商城”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“体育购物商城”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 系统总体模块设计 前台用户登录界面 系统首页界面…

数据篇| App爬虫入门(一)

App 的爬取相比 Web 端爬取更加容易,反爬虫能力没有那么强,而且数据大多是以 JSON 形式传输的,解析更加简单。在 Web 端,我们可以通过浏览器的开发者工具监听到各个网络请求和响应过程,在 App 端如果想要查看这些内容就需要借助抓包软件。常见抓包软件有: ‌工具名称‌‌…

go context学习

1.Context接口2.emptyCtx3.Deadline()方法4.Done()方法5.Err方法6.Value方法&#xff08;&#xff09;7.contex应用场景8.其他context方法 1.Context接口 Context接口只有四个方法&#xff0c;以下是context源码。 type Context interface {Deadline() (deadline time.Time, …

在VMware Workstation Pro上轻松部署CentOS7 Linux虚拟机

首先我们需要下载VM虚拟机和Centos7的镜像 下载并安装VMware Workstation Pro 访问VMware Workstation Pro官网下载 https://www.vmware.com/ 第二步&#xff1a;下载centos7镜像 访问centos官网下载 https://www.centos.org/ 开始部署Centos7 点击创建新的虚拟机 这里是Cen…

Jsoup 解析商品信息时需要注意哪些细节?

在使用Jsoup解析商品信息时&#xff0c;需要注意以下细节和最佳实践&#xff0c;以确保爬虫的稳定性和数据的准确性&#xff1a; 1. 检查HTML文档的合法性 在解析之前&#xff0c;需要确认所解析的文档是否是一份合法正确的HTML文档。如果HTML结构不完整或存在错误&#xff0…

Android AudioFlinger(五)—— 揭开AudioMixer面纱

前言&#xff1a; 在 Android 音频系统中&#xff0c;AudioMixer 是音频框架中一个关键的组件&#xff0c;用于处理多路音频流的混音操作。它主要存在于音频回放路径中&#xff0c;是 AudioFlinger 服务的一部分。 上一节我们讲threadloop的时候&#xff0c;提到了一个函数pr…

go的”ambiguous import in multiple modules”

执行“go mod tidy”报如下错误&#xff1a; go mod tidy -compat1.17 go: finding module for package github.com/gomooon/goredis go: found github.com/gomooon/goredis in github.com/gomooon/goredis v0.3.5 go: github.com/gomooon/core importsgithub.com/gomooon/gor…

从0开始的操作系统手搓教程27:下一步,实现我们的用户进程

目录 第一步&#xff1a;添加用户进程虚拟空间 准备冲向我们的特权级3&#xff08;用户特权级&#xff09; 讨论下我们创建用户线程的基本步骤 更加详细的分析代码 用户进程的视图 说一说BSS段 继续看process.c中的函数 添加用户线程激活 现在&#xff0c;我们做好了TSS…

Java线程池深度解析,从源码到面试热点

Java线程池深度解析&#xff0c;从源码到面试热点 一、线程池的核心价值与设计哲学 在开始讨论多线程编程之前&#xff0c;可以先思考一个问题&#xff1f;多线程编程的原理是什么&#xff1f; 我们知道&#xff0c;现在的CUP是多核CPU&#xff0c;假设你的机器是4核的&#x…

大数据技术在土地利用规划中的应用分析

大数据技术在土地利用规划中的应用分析 一、引言 土地利用规划是对一定区域内的土地开发、利用、整治和保护所作出的统筹安排与战略部署,对于实现土地资源的优化配置、保障社会经济的可持续发展具有关键意义。在当今数字化时代,大数据技术凭借其海量数据处理、高效信息挖掘等…

Node 使用 SSE 结合redis 推送数据(echarts 图表实时更新)

1、实时通信有哪些实现方式&#xff1f; 特性轮询&#xff08;Polling&#xff09;WebSocketSSE (Server-Sent Events)通信方向单向&#xff08;客户端 → 服务端&#xff09;双向&#xff08;客户端 ↔ 服务端&#xff09;单向&#xff08;服务端 → 客户端&#xff09;连接方…

Android Native 之 文件系统挂载

一、文件系统挂载流程概述 二、文件系统挂载流程细节 1、Init启动阶段 众所周知&#xff0c;init进程为android系统的第一个进程&#xff0c;也是native世界的开端&#xff0c;要想让整个android世界能够稳定的运行&#xff0c;文件系统的创建和初始化是必不可少的&#xff…

Redis--Set类型

目录 一、引言 二、介绍 三、命令 1.sadd,smembers,sismember 2.spop&#xff0c;srandmember 3.smove&#xff0c;srem 4.sinter&#xff0c;sinterstore 5.sunion,sunionstore,sdiff,sdiffstore 四、内部编码 1.intset 2.hashtable 五、应用场景 1.使用Set保存用…