提示:
- 所有体系课见专栏: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
。