使用go泛型对hertz框架封装一把,减少日常开发重复工作量

1,215 阅读4分钟

使用hertz开发接口

使用hertz开发一个用户注册接口示例

  1. 目录结构如下

image.png main文件的同级目录会存在一个router文件用于注册路由信息使用 biz/handler/xxx.go用于编写具体的业务代码

  1. 代码如下

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(&params); err != nil {  
        c.JSON(400, map[string]any{  
            "code": "1001",  
            "message": "illegal param",  
        })  
        return  
    }  
    // ...  
}

一般都是这种套路。但是这种写法有着很多缺点,如:

  1. 参数校验会存在大量重复的编码,我们的每一个业务代码都需要手动的去调用和序列化请求参数。
  2. 错误处理跟go平时写的代码不一样,go日常的写法是方法的返回值有两个,一个是结果,一个是error,但是hertz给我们提供的这种很明显,没有返回值,如果遇到错误,需要再业务代码中返回具体的错误响应。这对于程序员的心智有很大的挑战。

基于泛型封装一下上面的通用处理逻辑

  1. 声明一个bizFunc的函数,用于约定业务请求的函数签名
  2. HandlerFuncWrapper中,入参就是上面声明的bizFunc函数,出参是标准的hertz注册http路由时,使用的函数。 3.HandlerFuncWrapper具体代码逻辑非常简单
    1. 就是初始化泛型请求变量,然后调用c.BindAndValidate(&req)对参数校验和绑定。
    2. 如果出现异常,直接返回异常状态码,如果绑定参数成功,调用传入bizFunc函数,进行对应的业务请求处理。
    3. 这个bizFunc返回值跟我们日常开发中编写的go函数的返回值是一样的,两个返回值,一个是结果,一个是错误。
    4. 执行完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,  
        })  
    }  
}

改造后的用户注册登录代码如下:

  1. registerRouter注册路由时,使用HandlerFuncWrapper进行包装
func registerRouter(h *server.Hertz) {  
    h.POST("register", wrapper.HandlerFuncWrapper(handler.UserRegister))  
}
  1. 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提供的参数绑定的方法,貌似不支持二进制文件绑定到结构体上.... image.png

既然,hertz官方不支持,那么我们自己支持一下吧~~~

自定义tag并约定tag功能

  1. tag名称为 wrapper
  2. 定义一个结构体,用于存储表单上传文件的二进制以及二进制的信息
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  
}