使用go泛型对hertz框架封装一把,减少日常开发重复工作量
使用hertz开发接口
使用hertz开发一个用户注册接口示例
- 目录结构如下
main文件的同级目录会存在一个router文件用于注册路由信息使用
biz/handler/xxx.go用于编写具体的业务代码
- 代码如下
main.go
func main() {
h := server.Default()
registerRouter(h)
h.Spin()
}
router.go
func registerRouter(h *server.Hertz) {
h.POST("register", handler.UserRegister)
}
register.go
type UserRegisterParam struct {
Username string `json:"username" vd:"len($)>0"`
Password string `json:"password" vd:"len($)>0"`
}
func UserRegister(ctx context.Context, c *app.RequestContext) {
var params UserRegisterParam
if err := c.BindAndValidate(¶ms); err != nil {
c.JSON(400, map[string]any{
"code": "1001",
"message": "illegal param",
})
return
}
// ...
}
一般都是这种套路。但是这种写法有着很多缺点,如:
- 参数校验会存在大量重复的编码,我们的每一个业务代码都需要手动的去调用和序列化请求参数。
- 错误处理跟go平时写的代码不一样,go日常的写法是方法的返回值有两个,一个是结果,一个是error,但是hertz给我们提供的这种很明显,没有返回值,如果遇到错误,需要再业务代码中返回具体的错误响应。这对于程序员的心智有很大的挑战。
基于泛型封装一下上面的通用处理逻辑
- 声明一个
bizFunc
的函数,用于约定业务请求的函数签名 HandlerFuncWrapper
中,入参就是上面声明的bizFunc
函数,出参是标准的hertz
注册http路由时,使用的函数。 3.HandlerFuncWrapper
具体代码逻辑非常简单- 就是初始化泛型请求变量,然后调用
c.BindAndValidate(&req)
对参数校验和绑定。 - 如果出现异常,直接返回异常状态码,如果绑定参数成功,调用传入
bizFunc
函数,进行对应的业务请求处理。 - 这个bizFunc返回值跟我们日常开发中编写的go函数的返回值是一样的,两个返回值,一个是结果,一个是错误。
- 执行完bizFunc函数后,判断err是否为空,如果不为空,进行错误处理,并写错误的响应,如果为空,则证明本次业务请求成功了,直接将正确的响应返回即可。
- 就是初始化泛型请求变量,然后调用
type bizFunc[Req, Resp any] func(ctx context.Context, t Req) (resp Resp, err error)
func HandlerFuncWrapper[Req, Resp any](bizFunc bizFunc[Req, Resp]) app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
var req Req
// 空指针初始化
if reflect.TypeOf(req).Kind() == reflect.Ptr {
v := reflect.ValueOf(&req).Elem()
v.Set(reflect.New(v.Type().Elem()))
}
if err := c.BindAndValidate(&req); err != nil {
c.JSON(400, map[string]any{
"code": 400,
"message": err.Error(),
})
return
}
resp, err := bizFunc(ctx, req)
if err != nil {
// 错误处理,需要根据业务自己定制错误返回
// 这里简单的讲错误返回,并返回状态码500
c.JSON(500, map[string]any{
"code": 500,
"message": err.Error(),
})
return
}
// 业务执行成功,写成功响应
c.JSON(200, map[string]any{
"code": 0,
"message": "ok",
"data": resp,
})
}
}
改造后的用户注册登录代码如下:
- registerRouter注册路由时,使用HandlerFuncWrapper进行包装
func registerRouter(h *server.Hertz) {
h.POST("register", wrapper.HandlerFuncWrapper(handler.UserRegister))
}
- register.go
type UserRegisterParam struct {
Username string `json:"username" vd:"len($)>0"`
Password string `json:"password" vd:"len($)>0"`
}
func UserRegister(ctx context.Context, params *UserRegisterParam) (map[string]any, error) {
// 处理业务逻辑
// mock result
return map[string]any{
"user_id": "12345",
}, nil
}
改造后的代码变得非常的简洁,用户直接处理业务逻辑就好,不需要再关注参数绑定,写错误响应的问题了。处理http请求,就像编写日常的普通函数一样简单。
处理表单中上传文件
因为通过泛型封装后,编写业务代码时,不需要关注如何写响应信息了,所以,这里的bizFunc
的入参并没有app.RequestContext
参数。那么如何得到表单中的二进制文件呢?
解决这种问题最好的方式就是如果hertz提供的参数绑定如果支持绑定二进制文件,那么这个问题就解决了,但是hertz提供的参数绑定的方法,貌似不支持二进制文件绑定到结构体上....
既然,hertz官方不支持,那么我们自己支持一下吧~~~
自定义tag并约定tag功能
- tag名称为
wrapper
- 定义一个结构体,用于存储表单上传文件的二进制以及二进制的信息
type FormFileInfo struct {
Raw []byte // 文件二进制
Name string // 原始文件名称
Size int64 // 文件大小
}
编码实现
规范定义好后,我们直接根据规范,解析结构体标签写代码就行了
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/cloudwego/hertz/pkg/app"
"io"
"mime/multipart"
"reflect"
)
type FormFileInfo struct {
Raw []byte
Name string
Size int64
}
func (f *FormFileInfo) Reader() io.Reader {
return bytes.NewReader(f.Raw)
}
func injectWrapperTagValue(ctx context.Context, c *app.RequestContext, v any) error {
kind := reflect.TypeOf(v).Kind()
if kind != reflect.Ptr {
return errors.New("inject typeVal must be a ptr kind")
}
// 传入的v可能是一个多级指针,需要转换成一级指针
v = getFirstRankPtr(v)
if reflect.ValueOf(v).IsNil() {
// 空指针直接返回
return nil
}
typeVal := reflect.TypeOf(v).Elem() // 获取类型
vals := reflect.ValueOf(v).Elem() // 获取值
for i := 0; i < typeVal.NumField(); i++ {
tag := typeVal.Field(i).Tag
if tagVal, ok := tag.Lookup("wrapper"); ok {
// 存在tag
formFileInfo, err := getUploadFileInfoWithContext(ctx, c, tagVal)
if err != nil {
return err
}
if formFileInfo == nil {
return errors.New("parse form file failed, form file empty")
}
// 注入值
val := vals.Field(i)
if val.Kind() == reflect.Ptr {
// 指针类型的属性
if reflect.TypeOf(val.Interface()).Elem().Name() != "FormFileInfo" {
return fmt.Errorf("current field [%v] type not support `wrapper` tag", reflect.TypeOf(val.Interface()).Elem().Name())
}
val.Set(reflect.ValueOf(formFileInfo))
} else {
if val.Type().Name() != "FormFileInfo" {
return fmt.Errorf("current field [%v] type not support `wrapper` tag", val.Type().Name())
}
val.Set(reflect.ValueOf(*formFileInfo))
}
}
}
return nil
}
// 多级指针转换成一级指针
func getFirstRankPtr(v any) any {
temp := v
for reflect.TypeOf(temp).Kind() == reflect.Ptr {
v = temp
if reflect.ValueOf(temp).IsNil() {
// 空指针直接返回,不再取值
return temp
}
temp = reflect.ValueOf(temp).Elem().Interface()
}
return v
}
func getUploadFileInfoWithContext(ctx context.Context, c *app.RequestContext, filename string) (*FormFileInfo, error) {
raw, err := c.FormFile(filename)
if err != nil {
return nil, nil
}
fileInfo, err := getUploadFileInfo(raw)
if err != nil {
return nil, err
}
return fileInfo, nil
}
func getUploadFileInfo(fileHeader *multipart.FileHeader) (*FormFileInfo, error) {
openFile, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer openFile.Close()
data := make([]byte, fileHeader.Size+10)
_, err = openFile.Read(data) //读取传入文件的内容
if err != nil {
return nil, err
}
return &FormFileInfo{
Raw: data,
Name: fileHeader.Filename,
Size: fileHeader.Size,
}, nil
}
在HandlerFuncWrapper
添加解析自定义tag逻辑
type bizFunc[Req, Resp any] func(ctx context.Context, t Req) (resp Resp, err error)
func HandlerFuncWrapper[Req, Resp any](bizFunc bizFunc[Req, Resp]) app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
var req Req
// 空指针初始化
if reflect.TypeOf(req).Kind() == reflect.Ptr {
v := reflect.ValueOf(&req).Elem()
v.Set(reflect.New(v.Type().Elem()))
}
if err := c.BindAndValidate(&req); err != nil {
c.JSON(400, map[string]any{
"code": 400,
"message": err.Error(),
})
return
}
// 自定义标签解析
if err := injectWrapperTagValue(ctx, c, req); err != nil {
c.JSON(400, map[string]any{
"code": 400,
"message": err.Error(),
})
return
}
resp, err := bizFunc(ctx, req)
if err != nil {
// 错误处理,需要根据业务自己定制错误返回
// 这里简单的讲错误返回,并返回状态码500
c.JSON(500, map[string]any{
"code": 500,
"message": err.Error(),
})
return
}
// 业务执行成功,写成功响应
c.JSON(200, map[string]any{
"code": 0,
"message": "ok",
"data": resp,
})
}
}
这样就可以实现对文件二进制的绑定啦~。
使用示例
type UserAvatarUploadParam struct {
UserID string `form:"user_id" vd:"len($)>0"`
Avatar *wrapper.FormFileInfo `wrapper:"photo"`
}
func UserAvatarUpload(ctx context.Context, params *UserAvatarUploadParam) (map[string]any, error) {
// 处理业务逻辑
// mock result
return nil, nil
}
转载自:https://juejin.cn/post/7246321692196339768