当前位置: 首页 > 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 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

相关文章:

18 | 实现简洁架构的 Handler 层

提示&#xff1a; 所有体系课见专栏&#xff1a;Go 项目开发极速入门实战课&#xff1b;欢迎加入 云原生 AI 实战 星球&#xff0c;12 高质量体系课、20 高质量实战项目助你在 AI 时代建立技术竞争力&#xff08;聚焦于 Go、云原生、AI Infra&#xff09;&#xff1b;本节课最终…...

《Transformer如何进行图像分类:从新手到入门》

引言 如果你对人工智能&#xff08;AI&#xff09;或深度学习&#xff08;Deep Learning&#xff09;感兴趣&#xff0c;可能听说过“Transformer”这个词。它最初在自然语言处理&#xff08;NLP&#xff09;领域大放异彩&#xff0c;比如在翻译、聊天机器人和文本生成中表现出…...

coding ability 展开第三幕(滑动指针——基础篇)超详细!!!!

文章目录 前言滑动窗口长度最小的子数组思路 无重复字符的最长子串思路 最大连续1的个数思路 将x减到0的最小操作数思路 总结 前言 前面我们已经把双指针的一些习题练习的差不多啦 今天我们来学习新的算法知识——滑动窗口 让我们一起来探索滑动窗口的魅力吧 滑动窗口 滑动窗口…...

RAGFlow版本升级-Win10系统Docker

下载源码压缩包 https://github.com/infiniflow/ragflow.git 删除旧版本代码文件夹&#xff0c;把下载的代码解压到原先目录 更新一下env文件&#xff1a;ragflow/docker/.env 把值改为最新版本即可 RAGFLOW_IMAGEinfiniflow/ragflow:v0.17.1 更新一下docker docker compose -…...

通过mybatis的拦截器对SQL进行打标

1、背景 在我们开发的过程中&#xff0c;一般需要编写各种SQL语句&#xff0c;万一生产环境出现了慢查询&#xff0c;那么我们如何快速定位到底是程序中的那个SQL出现的问题呢&#xff1f; 2、解决方案 如果我们的数据访问层使用的是mybatis的话&#xff0c;那么我们可以通过…...

如何自己做奶茶,从此告别奶茶店

自制大白兔奶茶&#xff0c;奶香与茶香激情碰撞&#xff0c;每一口都是香浓与甜蜜的双重诱惑&#xff0c;好喝到跺脚&#xff01;丝滑口感在舌尖舞动&#xff0c;仿佛味蕾在开派对。 简单几步就能复刻&#xff0c;成本超低&#xff0c;轻松在家享受奶茶自由。 材料:大白兔奶糖&…...

JavaScript性能优化实战指南

JavaScript性能优化实战指南 1. 性能分析工具与指标 核心工具链 Chrome DevTools&#xff1a; Performance面板&#xff1a;记录运行时性能&#xff0c;分析长任务&#xff08;Long Tasks&#xff09;、强制回流&#xff08;Layout Shifts&#xff09;、函数调用堆栈。Memory面…...

宇树人形机器人开源模型

1. 下载源码 https://github.com/unitreerobotics/unitree_ros.git2. 启动Gazebo roslaunch h1_description gazebo.launch3. 仿真效果 H1 GO2 B2 Laikago Z1 4. VMware: vmw_ioctl_command error Invalid argument 这个错误通常出现在虚拟机环境中运行需要OpenGL支持的应用…...

【Linux】浅谈冯诺依曼和进程

一、冯诺依曼体系结构 冯诺依曼由 输入设备、输出设备、运算器、控制器、存储器 五部分组成。 冯诺依曼的设计特点 二进制表示 所有数据&#xff08;包括程序指令&#xff09;均以二进制形式存储和运算&#xff0c;简化了硬件逻辑设计&#xff0c;提高了可靠性。 存储程序原理…...

env.development.local 和 env.development 的区别

env.development.local 和 env.development 的区别 区别1、场景2、git管理3、加载策略 思考原因如下 区别 1、场景 env.development: 用于开发环境的环境变量配置env.development.local: 用于存储特定于开发者的本地配置信息 2、git管理 env.development.local 会通过*.loca…...

linux操作系统实战

第一题 创建根目录结构中的所有的普通文件 [rootlocalhost ~]# cd /[rootlocalhost /]# mkdir /text[rootlocalhost /]# cd /text[rootlocalhost text]# mkdir /text/boot /text/root /text/home /text/bin /text/sbin /text/lib /text/lib64 /text/usr /text/opt /text/etc /…...

Python Cookbook-4.1 对象拷贝

任务 想拷贝某对象。不过&#xff0c;当你对一个对象赋值&#xff0c;将其作为参数传递&#xff0c;或者作为结果返回时&#xff0c;Python 通常会使用指向原对象的引用&#xff0c;并不是真正的拷贝。 解决方案 Python 标准库的 copy 模块提供了两个函数来创建拷贝。第一个…...

浅谈时钟启动和Systemlnit函数

时钟是STM32的关键&#xff0c;是整个系统的心脏&#xff0c;时钟如何启动&#xff0c;时钟源如何选择&#xff0c;各个参数如何设置&#xff0c;我们从源码来简单分析一下时钟的启动函数Systemlnit&#xff08;&#xff09;。 Systemlnit函数简介 我们先来看一下源程序的注释…...

事业单位ABCDE类

1 我刚刚查阅了一下安徽省市直单位报名的表&#xff0c;我这个专业报的岗位大多数是自然科学专技岗。 2 安徽省的岗位大多都限制计算机科学与技术&#xff0c;我这个0854计算机技术能报的岗位十分有限。 而且我没有看到一个岗位只招应届生&#xff0c;显然安徽不保护应届生的…...

Python:函数(一)

python函数相关的知识点 1. 函数定义与调用 定义&#xff1a;使用 def 关键字&#xff0c;后接函数名和参数列表。 def greet(name):"""打印问候语&#xff08;文档字符串&#xff09;"""print(f"Hello, {name}!") 调用&#xff1a…...

MySql学习_基础Sql语句

目录 1.数据库相关概念 2.SQL 2.1 SQL通用语法 2.2 SQL分类 2.3 DDL&#xff08;数据库定义语言&#xff09; 2.4 DML&#xff08;数据操作语言&#xff09; 2.5 DQL&#xff08;数据查询语言&#xff09; 2.6 DCL&#xff08;数据控制语言&#xff09; 3. 函数 3.1 字…...

Nginx 生产环境安全配置加固

以下是Nginx生产环境安全配置加固的综合方案&#xff0c;结合多个技术实践和行业标准整理&#xff1a; 一、基础安全防护 1‌. 隐藏版本信息‌ 在http或server块添加server_tokens off;&#xff0c;避免暴露Nginx版本号‌。使用headers-more-nginx-module模块彻底移除响应头…...

C#中继承的核心定义‌

1. 继承的核心定义‌ ‌继承‌ 是面向对象编程&#xff08;OOP&#xff09;的核心特性之一&#xff0c;允许一个类&#xff08;称为‌子类/派生类‌&#xff09;基于另一个类&#xff08;称为‌父类/基类‌&#xff09;构建&#xff0c;自动获得父类的成员&#xff08;字段、属…...

小白学Agent技术[5](Agent框架)

文章目录 Agent框架Single Agent框架BabyAGIAutoGPTHuggingGPTHuggingGPT工作原理说明GPT-EngineerAppAgentOS-Copilot Multi-Agent框架斯坦福虚拟小镇TaskWeaverMetaGPT微软UFOAgentScope现状 常见Agent项目比较概述技术规格和能力实际应用案例开发体验比较ChatChain模式 Agen…...

21.dirsearch:Web 路径扫描工具

一、项目介绍 dirsearch 是一款高效、多线程的 Web 路径扫描工具&#xff0c;专为渗透测试人员和网络安全研究人员设计&#xff0c;用于发现目标网站的隐藏目录、敏感文件及未授权接口。其支持自定义字典、代理配置、请求头伪装等功能&#xff0c;适用于红队渗透、漏洞挖掘及资…...

VSTO(C#)Excel开发4:打印设置

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…...

设计模式Python版 模板方法模式(上)

文章目录 前言一、模板方法模式二、模板方法模式示例 前言 GOF设计模式分三大类&#xff1a; 创建型模式&#xff1a;关注对象的创建过程&#xff0c;包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式和建造者模式。结构型模式&#xff1a;关注类和对象之间…...

源IP泄露后如何涅槃重生?高可用架构与自动化防御体系设计

一、架构层解决方案 1. 高防代理架构设计 推荐架构&#xff1a; 用户 → CDN&#xff08;缓存静态资源&#xff09; → 高防IP&#xff08;流量清洗&#xff09; → 源站集群&#xff08;真实IP隐藏&#xff09; ↑ Web应用防火墙&#xff08;WAF&#xff09; 实施要点&a…...

transformer bert 多头自注意力

输入的&#xff08;a1,a2,a3,a4&#xff09;是最终嵌入&#xff0c;是一个(512,768)的矩阵&#xff1b;而a1是一个token&#xff0c;尺寸是768 a1通过wq权重矩阵&#xff0c;经过全连接变换得到查询向量q1&#xff1b;a2通过Wk权重矩阵得到键向量k2&#xff1b;q和k点乘就是值…...

python-leetcode-定长子串中元音的最大数目

1456. 定长子串中元音的最大数目 - 力扣&#xff08;LeetCode&#xff09; 可以使用 滑动窗口 方法来解决这个问题。步骤如下&#xff1a; 初始化&#xff1a;计算前 k 个字符中元音字母的个数&#xff0c;作为初始窗口的值。滑动窗口&#xff1a;遍历字符串&#xff0c;每次右…...

Spring Boot + MyBatis-Plus 项目目录结构

以下是一个标准的 Spring Boot MyBatis-Plus 项目目录结构及文件命名规范&#xff0c;包含每个目录和文件的作用说明&#xff0c;适用于中大型项目开发&#xff1a; 项目根目录结构 src/ ├── main/ │ ├── java/ # Java 源代码 │ │ └── com/…...

Python之变量及简单的数据类型

本文来源于《Python从入门到实践》&#xff0c;自己整理以供工作参考 基本内容 print("Hello Python World!")message "Hello Python world!" print(message)message "Helllo Python Crash Course world!" print(message)name "ada lov…...

力扣 Hot 100 刷题记录 - 翻转二叉树

力扣 Hot 100 刷题记录 - 翻转二叉树 题目描述 翻转二叉树 是力扣 Hot 100 中的一道经典题目&#xff0c;题目要求如下&#xff1a; 给你一棵二叉树的根节点 root&#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例 1&#xff1a; 输入&#xff1a;root [4,2,7…...

力扣215.数组中的第K个最大元素--堆排序法(java)

为了找到数组中第K个最大的元素&#xff0c;我们可以使用堆排序的方法。堆排序的核心是构建一个最大堆&#xff0c;并通过多次交换堆顶元素来找到前K个最大的元素。具体步骤如下&#xff1a; 方法思路 构建最大堆&#xff1a;将输入数组转换为最大堆&#xff0c;使得每个父节…...

MySQL增删改查操作 -- CRUD

个人主页&#xff1a;顾漂亮 目录 1.CRUD简介 2.Create新增 使用示例&#xff1a; 注意点&#xff1a; 3.Retrieve检索 使用示例&#xff1a; 注意点&#xff1a; 4.where条件查询 前置知识&#xff1a;-- 运算符 比较运算符 使用示例&#xff1a; 注意点&#xf…...