运用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
具体代码逻辑十分简略
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
}