0

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

 2 years ago
source link: https://studygolang.com/articles/35174
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

ormissia · 2天之前 · 259 次点击 · 预计阅读时间 7 分钟 · 大约8小时之前 开始浏览    

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/[email protected]拉取指定版本,目前尚不清楚 pkg.go.dev是否会自动同步GitHub上最新的tag

测试用例?
好吧,// TODO

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


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

280

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK