趁周末写了个小工具 - Golang 实体参数校验器

ormissia · 2021-08-03 15:32:36 · 1455 次点击 · 预计阅读时间 7 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2021-08-03 15:32:36 的文章,其中的信息可能已经有所发展或是发生改变。

A:"请用一句话让别人知道你写过Golang。"
B:"if err!= nil ..."

起因

只要是接触过Golang的人,无不为其if err != nil的语法感到惊奇,或是大加赞赏,或是狠狠痛批。作为使用者,不管喜欢也好,反对也罢, 目前还是要接受这种错误处理模式。

而最令人头痛的就是请求参数中各种值的校验。比如Get请求中接收分页参数时,需要将string格式的参数转换成int类型,再如时间类型的参数 转换, 诸如此类,等等等等。好家伙,一个接口写完if err != nil的判断占了一多半的行数,看着实在不爽。

下面就是一个典型的例子,而且这个接口参数还不是特别多

func Export(c *gin.Context) {
    //删除开头
    //...
    var param map[string]string
    err := c.ShouldBindJSON(&param)
    if err != nil {
        ErrRsponse(c,errCode)
        return
    }
    var vId, userId, userName, format string
    if v, ok := param["vId"]; ok {
        vId = v
    } else {
        ErrRsponse(c,errCode)
        return
    }

    if len(vId) == 0 {
        ErrRsponse(c,errCode)
        return
    }

    if v, ok := param["userId"]; ok {
        userId = v
    } else {
        ErrRsponse(c,errCode)
        return
    }
    if v, ok := param["userName"]; ok {
        userName = v
    } else {
        ErrRsponse(c,errCode)
        return
    }
    if v, ok := param["format"]; ok {
        format = v
    } else {
        ErrRsponse(c,errCode)
        return
    }
    if !file.IsOk(format) {
        ErrRsponse(c,errCode)
        return
    }
    //...
    //删除结尾
}

机遇

前几天在看GIN-VUE-ADMIN代码的时候,偶然看到一个通过反射去做参数校验的方式。 嘿,学到了!

改变

定义规则

校验规则使用一个map存储,key为字段名,value为规则列表,并使用一个string类型的切片来存储。

后续计划加入tag标签定义规则的功能以及增加通过函数参数的方式,实现自定义规则校验

type Rules map[string][]string

支持的规则有:

  • 不为空
  • 等于、不等于
  • 大于、小于
  • 大于等于、小于等于

对于数值类型为比较值大小,对于字符串或者切片等类型为比较长度大小

比如调用生成小于规则的方法,则会返回一个小于指定值规则的字符串,用于后面校验器使用

// Lt <
func (verifier verifier) Lt(limit string) string {
    return fmt.Sprintf("%s%s%s", lt, verifier.separator, limit)
}

规则定义示例:

    UserRequestRules = go_opv.Rules{
        "Name": {myVerifier.NotEmpty(), myVerifier.Lt("10")},
        "Age":  {myVerifier.Lt("100")},
    }
    //map[Age:[lt#100] Name:[notEmpty lt#10]]

规则含义为Age字段长度或值小于100,Name字段不为空且长度或值小于10。

验证器

先通过反射获取待检验参数的值和类型,判断是否为struct(目前只实现了对struct校验的功能,计划后续加入对map的校验功能), 获取struct属性数量并遍历所有属性,并遍历每个字段下所有规则,对定义的每一个规则进行校验是否合格。

func (verifier verifier) Verify(st interface{}, rules Rules) (err error) {
    typ := reflect.TypeOf(st)
    val := reflect.ValueOf(st)

    if val.Kind() != reflect.Struct {
        return errors.New("expect struct")
    }
    num := val.NumField()
    //遍历需要验证对象的所有字段
    for i := 0; i < num; i++ {
        tagVal := typ.Field(i)
        val := val.Field(i)
        if len(rules[tagVal.Name]) > 0 {
            for _, v := range rules[tagVal.Name] {
                switch {
                case v == "notEmpty":
                    if isEmpty(val) {
                        return errors.New(tagVal.Name + " value can not be nil")
                    }
                case verifier.conditions[strings.Split(v, verifier.separator)[0]]:
                    if !compareVerify(val, v, verifier.separator) {
                        return errors.New(tagVal.Name + " length or value is illegal," + v)
                    }
                }
            }
        }
    }
    return nil
}

规则校验有两种,分别是判空 和条件校验。
判空是通过反射reflect.Value获得字段值,并通过反射value.Kind()获得字段类型。 最终使用switch分别对不同类型 字段进行判断。

func isEmpty(value reflect.Value) bool {
    switch value.Kind() {
    case reflect.String:
        return value.Len() == 0
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        return value.Int() == 0
    //此处省略其他类型判断
    //...
    }
    return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}

条件校验则是通过开始时定义的范围条件进行校验,传入反射reflect.Value获得字段值,定义的规则,以及规则中的分隔符。先通过switch判断其类型, 再通过switch判断条件是大于小于或是其他条件,然后进行相应判断。

func compareVerify(value reflect.Value, verifyStr, separator string) bool {
    switch value.Kind() {
    case reflect.String, reflect.Slice, reflect.Array:
        return compare(value.Len(), verifyStr, separator)
    //此处省略其他类型判断
    //...
    default:
        return false
    }
}

封装

为了调用方便,做了一层封装,使用函数选项模式对校验器进行封装,使调用更为方便。

var defaultVerifierOptions = verifierOptions{
    separator: ":",
    conditions: map[string]bool{
        eq: true,
        ne: true,
        gt: true,
        lt: true,
        ge: true,
        le: true,
    },
}

type VerifierOption func(o *verifierOptions)
type verifierOptions struct {
    conditions map[string]bool
    separator  string
}

// SetSeparator Default separator is ":".
func SetSeparator(seq string) VerifierOption {
    return func(o *verifierOptions) {
        o.separator = seq
    }
}

func SwitchEq(sw bool) VerifierOption {
    return func(o *verifierOptions) {
        o.conditions[eq] = sw
    }
}

//...
//此处省略其他参数的设置

type Verifier interface {
    Verify(obj interface{}, rules Rules) (err error)

    NotEmpty() string
    Ne(limit string) string
    Gt(limit string) string
    Lt(limit string) string
    Ge(limit string) string
    Le(limit string) string
}

type verifier struct {
    separator  string
    conditions map[string]bool
}

func NewVerifier(opts ...VerifierOption) Verifier {
    options := defaultVerifierOptions
    for _, opt := range opts {
        opt(&options)
    }
    return verifier{
        separator:  options.separator,
        conditions: options.conditions,
    }
}

//...
//此处省略接口的实现

发布

好了,基本功能完成了,如果仅仅是放在每个项目的utils拷来拷去,显然十分的不优雅。
那么这就需要发布到pkg.go.dev才能通过go get命令正常被其他项目所引用。

  1. 首先是git commitgit push一把梭将项目整到GitHub上。
  2. 由于pkg.go.dev的版本管理机制需要给项目打上taggit tag v0.0.1基础版本,😋先定个0.0.1吧, 然后git push再走一遍。
  3. 当然这时候还没完,需要自己go get一下,加上GitHub仓库名执行一下go get github.com/ormissia/go-opv
  4. 这样仓库就可以正常被引用了。而且用不了多久,就可以从pkg.go.dev上搜到相应的项目了。
  5. 最后贴一下次项目的连接:go-opv

当然,这个过程中也遇到过小坑。项目中go.mod中的模块名需要写GitHub的仓库地址,对应此项目即为module github.com/ormissia/go-opv。 如果项目版本有更新,打了新的tag之后。可以通过go get github.com/ormissia/go-opv@v0.0.3拉取指定版本,目前尚不清楚 pkg.go.dev是否会自动同步GitHub上最新的tag

检验

测试用例?
好吧,// TODO

老铁看到底了,来个star吧😁
↓↓↓↓↓↓↓↓↓
GitHub仓库


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1455 次点击  ∙  1 赞  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传