提示:
- 所有体系课见专栏:Go 项目开发极速入门实战课;
- 欢迎加入 云原生 AI 实战 星球,12+ 高质量体系课、20+ 高质量实战项目助你在 AI 时代建立技术竞争力(聚焦于 Go、云原生、AI Infra);
- 本节课最终源码位于 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 文件中,内容如下:
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
路由方法代码如下:
go
// 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 文件中,内容如下:
go
// Validator 是验证逻辑的实现结构体.
type Validator struct {
// 有些复杂的验证逻辑,可能需要直接查询数据库
// 这里只是一个举例,如果验证时,有其他依赖的客户端/服务/资源等,
// 都可以一并注入进来
store store.IStore
}
// NewValidator 创建一个新的 Validator 实例.
func NewValidator(store store.IStore) *Validator {
return &Validator{store: store}
}
在 Validator
结构体中,可以添加校验逻辑中依赖的依赖项。例如 store.IStore
类型的实例,第三方微服务客户端等。以此实现更加复杂的校验逻辑。
ValidateCreateUserRequest 方法实现如下:
go
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 文件,添加以下代码:
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
方法实现如下:
go
// 注册 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:
go
$ ./build.sh
$ _output/fg-apiserver -c configs/fg-apiserver.yaml
打开另一个 Linux 终端,执行以下命令测试 HTTP 接口是否正常工作:
bash
$ curl -XPOST -H'Content-Type: application/json' http://127.0.0.1:6666/v1/users -d '{"username":"colin","password":"fastgo1234","nickname":"belm","email":"[email protected]","phone":"1818888xxxx"}'
{"userID":"user-gxqfqn"}
上述命令创建了一个新的用户,并返回了 用户 ID user-gxqfqn
。