#golang #reflect


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仓库