gin 自动映射参数及自动校验

近期在学习gin的时候发现对请求参数的校验很麻烦, 且重复代码很多, 进行一番思考和实践后发现了一种使用反射实现在 controller 函数上实现自动提取请求参数到指定的 struct, 并且自动使用 validation 进行校验.

起因

如下, 这是一段很普通的处理登录的代码, 获取请求参数, 验证参数是否正确在错误的时候返回错误码, 这种代码在项目中很常见, 很重复, 写这种代码总是让人厌烦.

type LoginParam struct {
    Account  string `validate:"required" json:"account"`
    Password string `validate:"required" json:"password"`
}

func main(){
  g := gin.New()
  g.POST("login", LoginHandlerFunc)
  _ = g.Run(":8080")
}

// HanlderFunc
func LoginHandlerFunc(ctx *gin.Context) {
    param := LonginParam{}
    err := ctx.ShouldBind(&param)
    if err != nil {
       ctx.String(400, "请求参数异常")
       return
    }
        secc, msg := Validate(&param)
        if !secc {
            ctx.String(400, msg)
            return
    }
        // ... CRUD
}

以下是验证器, 省略了翻译器的注册等代码.

import (
    "github.com/go-playground/validator/v10"
)

var v = validator.New()

func Validate(param interface{}) (bool, string) {
      err := v.Struct(param)
      errs := err.(validator.ValidationErrors)
        if len(errs) > 0 {
            err := errs[0]
            return false, err.Translate(that.trans)
        }
        return true, ""
}

目标

映射参数和校验参数都是固定的, 目标是将 LoginHandlerFunc 优化成如下所示.

func LoginHandlerFunc(ctx *gin.Context, params *LonginParam) {
    // ... CRUD
}

思路

问题一: 如何根据函数参数实现自动创建

我无法知道请求参数所映射 struct 的具体类型, 那只能使用反射, 可以反射获取到函数的参数, 具体实践如下.

func reflectHandlerFunc(handlerFunc interface{}){
    funcType := reflect.TypeOf(handlerFunc)
    // 判断是否 Func
    if funcType.Kind() != reflect.Func {
        panic("the route handlerFunc must be a function")
    }
    // 获取第二个参数的类型
    typeParam := funcType.In(1).Elem()
    // 创建实例
    instance := reflect.New(typeParam).Interface()
}

如上, 通过反射 handlerFunc 然后获取函数的第二个参数即可拿到 Type 然后再通过反射创建实例.

问题二: 如何映射参数并且验证

针对问题一中的代码进行一下优化.

func proxyHandlerFunc(ctx *gin.Context, handlerFunc interface{}){
    funcType := reflect.TypeOf(handlerFunc)
  funcValue := reflect.ValueOf(handlerFunc)

    // 判断是否 Func
    if funcType.Kind() != reflect.Func {
        panic("the route handlerFunc must be a function")
    }
    // 获取第二个参数的类型
    typeParam := funcType.In(1).Elem()
    // 创建实例
    param := reflect.New(typeParam).Interface()
    // 绑定参数到 struct
    err := ctx.ShouldBind(&param)
    if err != nil {
        ctx.String(400, "请求参数异常")
        return
    }
    // 验证参数
    succ, msg := Validate(&param)
    if !succ {
        ctx.String(400, msg)
        return
    }
    // 调用真实 HandlerFunc
    reflect.Call(valOf(ctx, param))
}

func valOf(i ...interface{}) []reflect.Value {
    var rt []reflect.Value
    for _, i2 := range i {
        rt = append(rt, reflect.ValueOf(i2))
    }
    return rt
}

如此, 就完成了一个对 HandlerFunc 的代理工作, 只要我们在注册路由时包装一下真实 HandlerFunc 即可.


// ...
g.POST("login", getHandlerFunc(LoginHandlerFunc))
// ...

func getHandlerFunc(handlerFunc interface{}) func(*gin.Context) {
    return func(context *gin.Context){
        proxyHandlerFunc(context, handlerFunc)
    }
}

// ...

func LoginHandlerFunc(ctx *gin.Context, param *LoginParam){
        // ... CRUD
}

代码及性能优化

0.兼容原有 HandlerFunc

假设项目已经进行到了一半, 而我无法对所有 HandlerFunc 进行一步到位的重构, 则需要兼容原来的方法, 这个只需加一个简单的判断即可.

func getHandlerFunc(handlerFunc interface{}) func(*gin.Context) {
    // 获取参数数量
    paramNum := reflect.TypeOf(handlerFunc).NumIn()
    valueFunc := reflect.ValueOf(handlerFunc)
    return func(context *gin.Context){
        // 只有一个参数说明是未重构的 HandlerFunc
        if paramNum == 1 {
                valueFunc.Call(valOf(context))
                return
        }
        proxyHandlerFunc(context, handlerFunc)
    }
}

1.针对特定的参数进行手动绑定并且验证

在实际开发中, 可能部分接口是表单, 有些接口是 JSON, 有些是其他类型, 以上代码只能由 gin.ShouldBind 自动处理绑定到 struct 的过程, 针对这个问题, 给 Param 实现特定接口即可, 如果实现了我们就是用该接口的方法进行绑定, 具体实现如下.

type Deserialzer interface {
    DeserializeFrom(ctx *gin.Context) error
}

type LoginParam struct {
    Account  string `validate:"required" json:"account"`
    Password string `validate:"required" json:"password"`
}

func (that *LoginParam) DeserializeFrom(ctx *gin.Context) error {
        return ctx.ShouldBindWith(that, binding.FormPost)
}

经过以上改造, 我们将具体的绑定过程交给具体的 struct 自己, 对于所有实现了 Dserializer 接口的 struct 都进行自定义绑定, 之后只需要对 proxyHandlerFunc 进行一点小改动即可实现这个功能的适配.

func proxyHandlerFunc(ctx *gin.Context, handlerFunc interface{}){
    // ...
    // 创建实例
    param := reflect.New(typeParam).Interface()
    deser, ok := param.(Deserialzer)
    // 如果未实现 Deserializer 接口则说明该 struct 使用默认绑定过程即可.
    if !ok {
        // 绑定参数到 struct
        err := ctx.ShouldBind(&param)
        if err != nil {
            ctx.String(400, "请求参数异常")
            return
        }
    } else {
        // 绑定请求参数
        err := deser.DeserializeFrom(ctx)
        if err != nil {
            ctx.String(400, "请求参数异常")
            return
        }
        param = reflect.ValueOf(deser).Interface()      
    }
    // 验证参数
    succ, msg := Validate(&param)
    // ...
}

对于自定义参数验证过程也是按一样的方法即可实现.

性能优化

GO 的反射对性能的影响是巨大的, 因此应尽量避免在 HandleFunc 中使用反射, 以上功能使用反射和不使用耗时相差约300倍. 所以, 部分 Type, Value 可以在注册路由时进行反射, 提前反射, 就避免了每次使用都反射.

如下所示, 我们对真实 handlerFunc, 以及参数类型进行提前反射.

func GetHandlerFunc(handlerFunc interface{}) func(*gin.Context) {
    // 提前反射
    paramNum := reflect.TypeOf(handlerFunc).NumIn()
    funcValue := reflect.ValueOf(handlerFunc)
    funcType := reflect.TypeOf(handleFunc)
    paramType := funcType.In(1).Elem()

    // 判断是否 Func
    if funcType.Kind() != reflect.Func {
        panic("the route handlerFunc must be a function")
    }
    // ... 还可以做一些其他校验确保无误
    return func(context *gin.Context){
        // 只有一个参数说明是未重构的 HandlerFunc
        if paramNum == 1 {
                funcValue.Call(valOf(context))
                return
        }
        proxyHandlerFunc(context, funcValue, paramType)
    }
}

func proxyHandlerFunc(ctx *gin.Context, funcValue reflect.Value, typeParam reflect.Type){
    // 创建实例
    param := reflect.New(typeParam).Interface()
    // ...
    // 调用真实 HandlerFunc
    reflect.Call(valOf(ctx, param))
}

func valOf(i ...interface{}) []reflect.Value {
    var rt []reflect.Value
    for _, i2 := range i {
        rt = append(rt, reflect.ValueOf(i2))
    }
    return rt
}

性能测试

使用 Baenchmark 进行性能测试.

func BenchmarkNormalHandleFunc(b *testing.B) {
    router := gin.New()
    router.POST("login", func(ctx *gin.Context) {
        p := validates.RegisterParams{}
        validator := LoginParam{}
        if !validator.Validate(wrap.Context(ctx), &p) {
            return
        }
    })
    config.Router = router
    go func() {
        _ = router.Run(":8081")
    }()

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        testPost("8081")
    }
}
func BenchmarkReflectHandleFunc(b *testing.B) {
    router := gin.New()
    handlerFunc := func(ctx *gin.Context, params *LoginParam) {
        //...
    }
    router.POST("login", GetHandlerFunc(handleFunc))
    go func() {
        _ = router.Run(":8082")
    }()

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        testPost("8082")
    }
}

func testPost(port string) {
    params := struct {
        Account  string
        Password string
        Email    string
        Captcha  string
    }{
        Account:  "account",
        Password: "1231ljasd",
        Email:    "email@exmpale.com",
        Captcha:  "12345",
    }
    paramsByte, _ := json.Marshal(params)
    r, _ := http.Post("http://127.0.0.1:"+port+"/login", "application/json", bytes.NewReader(paramsByte))

    if r.StatusCode != 200 {
        fmt.Println("errr")
    }
}

测试结果如下.

E:\go> go test -bench="." -count=5 -benchmem
goos: windows
goarch: amd64
pkg: go
BenchmarkNormalHandleFunc-12               19603             59545 ns/op            7321 B/op         83 allocs/op
BenchmarkNormalHandleFunc-12               17412             66923 ns/op            7306 B/op         83 allocs/op
BenchmarkNormalHandleFunc-12               20368             57849 ns/op            7349 B/op         83 allocs/op
BenchmarkNormalHandleFunc-12               20542             60086 ns/op            7395 B/op         83 allocs/op
BenchmarkNormalHandleFunc-12               20577             58671 ns/op            7361 B/op         83 allocs/op
BenchmarkReflectHandleFunc-12              20613             58374 ns/op            7493 B/op         85 allocs/op
BenchmarkReflectHandleFunc-12              20230             62594 ns/op            7456 B/op         85 allocs/op
BenchmarkReflectHandleFunc-12              19764             59617 ns/op            7441 B/op         85 allocs/op
BenchmarkReflectHandleFunc-12              20684             58899 ns/op            7461 B/op         85 allocs/op
BenchmarkReflectHandleFunc-12              19796             58712 ns/op            7421 B/op         85 allocs/op
PASS
ok      go    18.177s

性能几乎没有损耗.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容