运用hertz开发接口

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

  1. 目录结构如下

使用go泛型对hertz框架封装一把,减少日常开发重复工作量
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供给的参数绑定的方法,形似不支撑二进制文件绑定到结构体上….

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

既然,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  
}