Golang 实体参数校验
#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(¶m)
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
命令正常被其他项目所引用。
- 首先是
git commit
、git push
一把梭将项目整到GitHub
上。 - 由于pkg.go.dev的版本管理机制需要给项目打上
tag
,git tag v0.0.1
基础版本,😋先定个0.0.1
吧, 然后git push
再走一遍。 - 当然这时候还没完,需要自己
go get
一下,加上GitHub
仓库名执行一下go get github.com/ormissia/go-opv
- 这样仓库就可以正常被引用了。而且用不了多久,就可以从pkg.go.dev上搜到相应的项目了。
- 最后贴一下次项目的连接: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仓库